Nginx 怎么自动封禁高频访问的爬虫 IP?
Nginx 的 limit_req 只能限速并返回 429/444,无法真正封禁 IP;需结合 fail2ban 扫描日志调用 iptables,或用 OpenResty 共享内存维护封禁表,并配合限速、白名单和并发限制。
Nginx 本身只能“限速”,不能“封禁”:limit_req 只会对超频请求返回 429 或 444,不会把 IP 拉进黑名单。要实现“自动封禁”,必须再加一层:用 fail2ban 扫描 Nginx 日志,或者用 OpenResty 的共享内存字典维护封禁表,把高频 IP 真正拦截。
limit_req 和“封禁”有什么区别?
结论:limit_req 基于漏桶算法做限速,超出速率的请求直接被拒并返回状态码,但下一个请求仍然会先进入计数、再被拒,因此无法封禁 IP,爬虫也不会失去访问资格。真正封禁的做法是让后续请求在 Nginx 的 access 阶段就被 return 444 断连,根本不进入业务逻辑。
先配置限速层,这是判断爬虫频率的基础。在 http 块中定义共享内存区,10MB 大约可以存 16 万个 IP 的计数状态:
http {
limit_req_zone $binary_remote_addr zone=crawler:10m rate=5r/s;
limit_conn_zone $binary_remote_addr zone=connip:10m;
limit_req_status 429;
limit_conn_status 429;
limit_req_log_level warn;
server {
location / {
limit_req zone=crawler burst=20 nodelay;
limit_conn connip 20;
proxy_pass http://backend;
}
}
}
几个参数必须理解:rate=5r/s 表示每 200 毫秒放行一个令牌;burst=20 nodelay 表示允许瞬时堆积 20 个请求并立即放行,超过这个数量才拒绝。nodelay 不加的话,超出部分会排队延迟处理,反而拖慢正常用户。limit_conn 20 可以补充限制单 IP 并发连接,防止爬虫靠长连接绕过速率限制。
$binary_remote_addr 比 $remote_addr 省一半内存,因为它用 4 字节或 16 字节二进制表示 IP,而不是文本。
怎么让内部 IP 和搜索引擎不受限速影响?
结论:用 map 把白名单 IP 的限速 key 置空,因为 Nginx 遇到空的 key 会直接跳过限速,这比在 if 里写 return 更可靠。
geo $limit_white {
default 1;
10.0.0.0/8 0;
192.168.0.0/16 0;
203.0.113.0/24 0; # 公司出口 IP
}
map $limit_white $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=crawler:10m rate=5r/s;
注意:不要只靠 User-Agent 放行 Googlebot。UA 可以被任意伪造。如果要放行搜索引擎,就对声称是 Googlebot 的 IP 做正向-反向 DNS 校验,或者直接维护官方 IP 段列表。
fail2ban 怎么自动拉黑高频 IP?
结论:fail2ban 读取 Nginx access log,在 findtime 秒内匹配到超过 maxretry 条 429 响应,就调用 iptables 封禁 bantime 秒;被封 IP 的请求在到达 Nginx 之前就被内核丢弃,开销为零。
日志格式必须让 IP 位于行首,否则 failregex 无法匹配:
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent"';
/etc/fail2ban/filter.d/nginx-crawler.conf:
[Definition]
failregex = ^<HOST> .* "(GET|POST|HEAD) [^"]*" 429
ignoreregex =
/etc/fail2ban/jail.d/nginx-crawler.conf:
[nginx-crawler]
enabled = true
filter = nginx-crawler
logpath = /var/log/nginx/access.log
maxretry = 60
findtime = 60
bantime = 3600
action = iptables-multiport[name=nginx-crawler, port="http,https"]
含义是 60 秒内被限速拒绝 60 次即封禁 1 小时。实测阈值设为 30-60 比较平衡:太低容易误封移动网络 NAT 出口(一整个小区共享一个公网 IP),太高则爬虫已经抓走不少页面。bantime 建议 3600 起步,对顽固爬虫可以加到 86400。
如果 Nginx 跑在 Docker/K8s 里,iptables 动作要换成 nginx-block-map(写黑名单文件再 include)或云厂商 WAF,因为容器内的 iptables 管不到宿主机入口流量。
OpenResty 怎么做“秒级封禁”?
结论:用 ngx.shared.DICT 在 Nginx 内部统计并封禁,延迟在微秒级,不依赖外部进程扫描日志,适合 QPS 上万、单机部署的场景。
-- access_by_lua_block
local ban = ngx.shared.ban
local cnt = ngx.shared.counter
local ip = ngx.var.binary_remote_addr
if ban:get(ip) then
return ngx.exit(444) -- 直接断连,不回任何内容
end
local key = "c:" .. ip
local n = cnt:incr(key, 1, 0, 60) -- 60 秒滑动窗口,首次写入即设过期
if n and n > 100 then
ban:set(ip, true, 3600) -- 封禁 1 小时
return ngx.exit(444)
end
incr(key, value, init, init_ttl) 的第 4 个参数用于设置计数键的初始过期时间,需要 lua-nginx-module 0.10.12 及以上。两个字典都要在 lua_shared_dict 里声明,例如 lua_shared_dict ban 10m;。
选 444 还是 429 要分清:444 是 Nginx 私有状态码,直接关闭连接,最省资源,但爬虫拿不到任何提示,会不停重试;429 加 add_header Retry-After 3600; 更符合 HTTP 语义,行为规范的爬虫会退避。对恶意爬虫用 444,对正常 API 限流用 429。
封禁失效最常见的三个原因
结论:90% 的“封了没用”都源于取到了错误的 IP,或者共享内存没跨机器同步。
第一,$remote_addr 取的是直连 IP。挂在 CDN 或负载均衡后面时,你封的是 CDN 节点,一封就是几万人。必须配置 set_real_ip_from 你的CDN段; 和 real_ip_header X-Forwarded-For;,并确认 CDN 已回传真实 IP。
第二,ngx.shared.DICT 只在单机内共享,多台 Nginx 各自统计,爬虫轮流打 10 台机器等于速率除以 10。要么在入口层统一限速,要么把计数写到 Redis 做全局统计。
第三,limit_req_zone 的 key 用了 $remote_addr 这个文本变量,内存占用翻倍。10MB 共享区用 $binary_remote_addr 能存约 16 万个 IP,用文本只能存 8 万个左右。
总结:limit_req + limit_conn 负责限速和识别,fail2ban 或 OpenResty 共享字典负责真正的封禁,白名单用 map 置空 key 放行,而所有方案成立的前提是 real_ip 配置正确、拿到真实客户端 IP。
转载请注明出处,版权归原作者所有。