Nginx 怎么按 User-Agent 配置爬虫黑名单?
用 map 将 $http_user_agent 映射为 0/1 变量并在 server 块以 if 返回 403 或 444,集中维护 UA 黑名单规则,避免在 if 或 location 中写正则;改完用 nginx -t 校验并 re
结论:Nginx 按 User-Agent 配置爬虫黑名单最省事、性能最好的做法是——先用 map 指令把 $http_user_agent 映射成一个 0/1 变量,再在 server 块里用一句 if ($变量) { return 403; } 拦掉。不要把正则直接写进 if 里,也别在 location 里堆一串 if。
User-Agent(简称 UA)是客户端在 HTTP 请求头里声明的身份字符串,爬虫、扫描器通常会把自己的名字写在里面,这正是黑名单的抓手。
为什么推荐 map + if,而不是直接在 if 里写正则?
结论:map 会把 UA 规则集中在 http 块里统一维护,匹配结果缓存为变量,server 块里只剩一次布尔判断,性能和维护成本都最低。
原因有三点。第一,map 只在 http 块定义一次,所有 server 共享,加规则改规则都在一个地方。第二,map 是 Nginx 的原生变量映射,规则在配置加载时就编译好,请求期只做一次查表或正则匹配,不像 if 里写复杂正则那样每次请求都重新解析表达式。第三,server 级别的 if 里只放 return,是 Nginx 官方认可的安全用法;真正臭名昭著的 “if is evil” 指的是 location 块里混用 if 和 proxy_pass、try_files 等指令,容易出现意料之外的匹配顺序。
完整配置怎么写?
结论:规则放 http 块,判断放 server 块,改完 nginx -t 校验,nginx -s reload 平滑生效。
map $http_user_agent $ua_bad {
default 0; # 默认放行,default 不能省
"" 1; # 空 UA,正常浏览器几乎不会发,直接拦
# 脚本/采集工具(~* 表示大小写不敏感的正则匹配)
~*python-requests 1;
~*python-urllib 1;
~*scrapy 1;
~*go-http-client 1;
~*okhttp 1;
~*curl 1;
~*wget 1;
~*libwww-perl 1;
# 商业 SEO 与 AI 采集器
~*AhrefsBot 1;
~*SemrushBot 1;
~*MJ12bot 1;
~*Bytespider 1;
~*GPTBot 1;
~*ClaudeBot 1;
~*CCBot 1;
# 漏洞扫描器
~*sqlmap 1;
~*nikto 1;
~*Nmap 1;
~*masscan 1;
~*zgrab 1;
}
# 2) 在 server 块顶部统一拦截
server {
listen 80;
server_name example.com;
if ($ua_bad) {
return 403; # 想更省资源可以改成 return 444
}
location / {
proxy_pass http://127.0.0.1:8080;
}
}
两个细节要记住。map 里的普通字符串是整串精确匹配,只有以 ~(大小写敏感)或 ~*(大小写不敏感)开头才是正则子串匹配,所以 ~*python-requests 才能命中 python-requests/2.31.0。另外 return 444 是 Nginx 特有的状态码,直接关闭连接、不回任何响应头,比 403 更省带宽,但排查时不好定位,建议先用 403 跑一周。
怎么验证配置真的生效?
结论:用 curl -A 指定 UA 打自己的域名,看返回码从 200 变成 403,同时看 access_log 有没有记录。
nginx -t && nginx -s reload
curl -I -A "python-requests/2.31.0" https://example.com/
# 期望:HTTP/2 403
curl -I -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0" https://example.com/
# 期望:HTTP/2 200
想单独统计被拦了多少,可以拆一个日志:
map $ua_bad $log_blocked {
1 1;
0 ""; # 注意这里必须映射成空串,不能是 0
}
access_log /var/log/nginx/ua_blocked.log combined if=$log_blocked;
这里有个坑:access_log ... if=$ua_bad 看似能用,但 if= 判断的是“变量值是否为空字符串”,而 0 是非空字符串,会被判成 true,结果所有请求都写进日志。所以要像上面那样把放行分支 map 成空串。
只按 UA 封,会不会误伤自己的服务?
结论:会,最常见的误伤是自己用 curl 写的健康检查、监控脚本和前端 SSR 请求,务必先加 IP 白名单再上线。
用 geo 配合组合 map 就能实现“白名单 IP 即使 UA 命中也不拦”:
geo $white_ip {
default 0;
10.0.0.0/8 1;
203.0.113.10 1; # 监控机
}
map "$white_ip$ua_bad" $ua_deny {
default 0;
"01" 1; # 白名单=0 且 黑名单=1 才拒绝
}
server {
if ($ua_deny) { return 403; }
}
顺带一提,curl、axios、okhttp 这几条规则误伤率最高,如果你的后台服务、App 客户端用了这些 UA,要么从黑名单里删掉,要么靠上面的白名单兜底。
光靠 UA 黑名单够不够?
结论:不够,UA 一行字符串随便伪造,黑名单只能挡掉“懒得改 UA”的低成本爬虫,必须叠加限速和连接数限制。
推荐同时挂上请求频率限制:
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn:10m;
server {
limit_req zone=perip burst=20 nodelay;
limit_conn conn 20;
}
rate=10r/s 表示单 IP 每秒 10 个请求,burst=20 允许瞬时突发 20 个,超出直接 503。这套组合对没有 IP 池的采集器效果很好;对方上了代理池,就得再往前一层加 WAF 或 JS 挑战了。
整套方案的核心就三步:http 块写 map 收规则,server 块 if 加 return 403 做拦截,上线前用 geo 白名单挡住自家监控流量,再配合 limit_req 补上频率这一层。UA 黑名单成本最低、见效最快,但它是第一道筛子而不是最后一道墙。
转载请注明出处,版权归原作者所有。