Nginx 防爬虫配置如何做速率限制分级?比如不同 UA 不同速率。
Nginx 不能按 UA 动态给 limit_req 指定速率,应为每个速率等级分别建 limit_req_zone,并用 map 生成仅本等级非空、其余为空的 key,使多条 limit_req 在同一 location 中互不干扰地实现
可以做到,而且不需要 OpenResty:Nginx 实现「按 UA 分速率」的正确做法是**一个速率等级对应一个 limit_req_zone,再用 map 为每个等级生成一个「本等级命中时为客户端 IP、其余情况为空字符串」的 key**;因为 key 为空时 Nginx 会直接跳过该条限速规则,所以同一个 location 里写多条 limit_req 也不会互相干扰。不要去尝试给一条 limit_req 动态传 rate,它不支持变量。
Nginx 的 limit_req 能按 UA 动态指定速率吗?
结论:不能。limit_req_zone 的 zone= 名称和 rate= 都是配置加载时确定的静态值,limit_req zone=xxx rate=yyy 这种带变量的写法 Nginx 会直接报 nginx: [emerg] invalid parameter。所以分级只能靠「多个 zone 各管一个等级」来实现。
真正可变的只有 limit_req_zone 的 key。key 是任意复杂变量,可以拼 $http_user_agent、$uri、$request_method。这里有个关键机制:在 ngx_http_limit_req_handler 中,key 长度为 0 时直接返回 NGX_DECLINED,即该条限速规则对这个请求完全不生效。分级的全部技巧都建立在这一点上。
不同 UA 不同速率怎么配?完整配置
先分类,再生成「可为空」的 key,最后为每个等级定义 zone:
map $http_user_agent $ua_search {
default 0;
"~*(googlebot|bingbot|baiduspider|yisouspider|bytespider)" 1;
}
map $http_user_agent $ua_script {
default 0;
"~*(python-requests|Scrapy|curl|wget|Go-http-client|okhttp|Java/|axios)" 1;
"" 1; # 无 UA 一律当脚本
}
# 2) 生成可空 key:不是本等级就置空
map $ua_search $key_search {
0 "";
1 $binary_remote_addr;
}
map $ua_script $key_script {
0 "";
1 $binary_remote_addr;
}
map $ua_search$ua_script $key_human {
"00" $binary_remote_addr;
default "";
}
# 3) 一个等级一个 zone,rate 写死在这里
limit_req_zone $key_human zone=rl_human:10m rate=10r/s;
limit_req_zone $key_search zone=rl_search:10m rate=3r/s;
limit_req_zone $key_script zone=rl_script:10m rate=1r/s;
limit_req_status 429;
limit_req_log_level warn;
然后在 location 里把三条都挂上:
location / {
limit_req zone=rl_human burst=20 nodelay;
limit_req zone=rl_search burst=10 nodelay;
limit_req zone=rl_script burst=3;
proxy_pass http://backend;
}
limit_req 是数组型指令,写多条会全部生效(不像多数指令是后者覆盖前者)。但由于普通浏览器的 $key_search、$key_script 为空,脚本 UA 的 $key_human 为空,一个请求实际只会命中一条规则,等效于「分级限速」。
burst 和 nodelay 应该怎么配?
结论:burst 决定允许的突发排队长度,nodelay 决定突发请求是立即处理还是排队等待,浏览器类要给大 burst + nodelay,脚本类要小 burst 且不加 nodelay。
- 人类访问:
rate=10r/s+burst=20 nodelay。一个页面带 40 个静态资源时并发上来不会误杀,这是最常见的误伤来源。 - 搜索引擎:
rate=3r/s+burst=10 nodelay。卡太死会直接影响收录,抓取压力大应该用sitemap和Crawl-delay控制,不要靠 Nginx 硬顶。 - 脚本 / 未知 UA:
rate=1r/s+burst=3,不加 nodelay,让它排队,队列满了返回 429。
limit_req_status 429; 必须显式设置,默认是 503,会被监控当成服务故障。上线前建议先加一行 limit_req_dry_run on;(Nginx 1.17.1 及以上),只打日志不拦截,观察一周 error.log 里 limiting requests, excess: ... by zone "rl_script" 的条数,再决定是否真的拒绝。
套了 CDN 之后限速为什么全乱了?
结论:没配 real_ip 模块时,$binary_remote_addr 拿到的是 CDN 回源节点的 IP,所有用户会被合并进同一个令牌桶,正常用户会被集体误杀。
必须在 http 或 server 块加:
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
配置完 nginx -t && nginx -s reload,再用 for i in $(seq 1 30); do curl -s -o /dev/null -w "%{http_code} " -A "python-requests/2.31" https://你的域名/; done 验证,正常应看到前面几个 200、后面连续 429。内存方面,10m 的 zone 在 64 位系统约可容纳 8 万个 IP 状态,中小站点够用。
UA 分级能挡住伪装爬虫吗?
结论:挡不住。UA 可以随意伪造,UA 分级只解决「正常类型差异化配额」的问题,不构成防护。
真正的防御要叠加 IP 维度:用 geo 加载恶意 IP / 机房 IP 段单独给一个 rate=1r/m 的 zone;或者上 OpenResty 做 JS 挑战、行为指纹。UA 分级的价值在于把「搜索引擎」「真实浏览器」「脚本」三类请求分开计量,避免爬虫把正常用户的配额吃光。
整套思路一句话总结:分级靠多 zone,切换靠空 key,rate 静态写死,burst 按业务定,CDN 场景先修 real_ip 再谈限速。这套「不同行为不同冷却」的思路并不新鲜,GJ站长论坛自身也是按行为分档的——发帖冷却 30 秒、回帖冷却 15 秒(见社区公告 https://www.gj0.com/announcements.html),本质上和 Nginx 给浏览器 10r/s、给脚本 1r/s 是一回事。配置过程中遇到的具体报错,可以到 GJ站长论坛的运维版块(子分类:运维、面板)贴日志讨论。
转载请注明出处,版权归原作者所有。