⭐ 推荐:社区规则条款 V1.0

Nginx 防爬虫配置中如何设置动态黑名单过期时间?

GJ论坛站长
GJ论坛站长 管理员资深会员 👑黑卡会员
发布于 2026-10-23 14:27 ·6 浏览 ·0 回复
内容摘要

Nginx 动态黑名单过期时间由存储层决定:原生 map/geo 无法自动过期,只能定时重写并 reload;OpenResty 通过 lua_shared_dict 的 exptime 设置过期,需注意惰性删除和重启丢失,阶梯封禁可分别存

Nginx 本身没有「黑名单过期时间」这个配置项,过期时间由承载黑名单的存储层决定:用 OpenResty 就走 ngx.shared.DICT:set(ip, 1, 600) 的第三个参数 exptime(单位秒),用 fail2ban 就调 bantime 与 bantime.increment,而纯原生 Nginx 的 map/geo 方案无法自动过期,只能靠定时任务重写配置再 nginx -s reload。结论:选哪种过期机制,先看你有没有 lua-nginx-module。

Nginx 原生为什么做不到黑名单自动过期?

结论:deny、map、geo 都是配置加载期生成的静态结构,只在 nginx -s reload 时重建,运行期没有任何「写入—失效」接口,所以原生方案不存在过期语义。

有人用 map $remote_addr $blocked { ... } 加一个定时脚本,把封禁 IP 追加到 blocklist.conf 再 reload。这条路可行但有明确代价:每次 reload 都要新旧 worker 平滑切换,IP 列表到 1 万条以上时 reload 耗时与内存翻倍的抖动会很明显,而且频繁 reload 会打断长连接。封禁量小(几百条)可以用,规模上来必须换存储。

lua_shared_dict 的过期时间怎么设置?

结论:在 http 块声明 lua_shared_dict blacklist 10m;,写入时用 dict:set(key, value, exptime) 指定秒数,到点后 dict:get() 自动返回 nil,无需任何清理任务。

http {
    lua_shared_dict blacklist 10m;
    lua_shared_dict ban_count 10m;

    server {
        access_by_lua_block {
            local ip = ngx.var.remote_addr
            if ngx.shared.blacklist:get(ip) then
                return ngx.exit(403)
            end
        }
    }
}

三个必须知道的细节:

  1. exptime 传 0 或省略表示永不过期,别把「0 = 立即过期」搞反。
  2. 过期是惰性删除:键到期后仍占着内存,直到被再次访问或被 LRU 淘汰。用 local n = dict:flush_expired(100) 主动清理,每次最多清 100 个,建议放在 ngx.timer.every(60, flush) 里。
  3. shared dict 是所有 worker 共享的同一块内存,nginx -s reload 期间数据保留,但 restart 或进程崩溃后全部丢失。要跨重启保留,得把黑名单落到 Redis。

容量按「每个键值约 100 字节元数据 + 键值本身」估算,10MB 大致能存几万条 IPv4 记录;写满后默认按 LRU 淘汰,会把仍在封禁期的 IP 挤掉,所以宁可开 20m 也别卡着用。

怎么做阶梯封禁(10 分钟 → 1 小时 → 24 小时)?

结论:用两个 dict,一个存违规次数(长 TTL,如 604800 秒),一个存当前封禁状态(短 TTL),按次数映射不同的 exptime 即可。

local bl  = ngx.shared.blacklist
local cnt = ngx.shared.ban_count
local ip  = ngx.var.remote_addr

if bl:get(ip) then return ngx.exit(403) end

-- 命中爬虫特征后执行:计数窗口 7 天
local n   = cnt:incr(ip, 1, 0, 604800)
local ttl = ({600, 3600, 86400})[math.min(n, 3)]
bl:set(ip, n, ttl)

dict:incr(key, value, init, init_ttl) 的后两个参数需要 OpenResty 1.11.2 以上版本支持,老版本要手动 get 后 set。

fail2ban + ipset 方案的过期怎么配?

结论:改 bantime 控制单次封禁时长,加 bantime.increment = true 实现递增封禁,这三项配置在 fail2ban 0.11.0 及以上版本生效。

[nginx-badbot]
enabled  = true
filter   = nginx-badbot
action   = iptables-ipset-proto6[name=badbot]
logpath  = /var/log/nginx/access.log
findtime = 600
maxretry = 30
bantime  = 3600
bantime.increment = true
bantime.factor    = 2
bantime.maxtime   = 86400

含义是首次封 1 小时,第二次 2 小时、第三次 4 小时…… 最长不超过 24 小时。ipset 层对应的是 timeout 参数,可以直接验证:ipset list badbot 能看到每条记录的剩余超时。

配过期时间最容易踩哪些坑?

结论:最大的坑是拿不到真实 IP 和过期时间单位弄错,前者让封禁误伤整段 CDN 节点,后者让封禁从 1 小时变成 1 秒。

  • 站前有 CDN 时,ngx.var.remote_addr 拿到的是回源节点地址。必须在 http 或 server 块加 set_real_ip_from 回源段; 与 real_ip_header X-Forwarded-For;,否则一条黑名单会把整个 CDN 段封掉。
  • lua_shared_dict 的 exptime 单位是秒,不是毫秒;bantime 支持 1h、1d 这类带单位写法,两种风格别混。
  • 拦截要放在 access_by_lua_block,放在 log_by_lua_block 只能记录不能拦截;判定逻辑要早于 content 阶段,否则静态文件已经被 try_files 处理掉了。
  • 别只封 IP:UA 特征、请求频率、robots.txt 之外的路径扫描,建议和 IP 黑名单叠加使用,避免同一出口 IP 下正常用户被误伤。

总的来说,动态黑名单的「动态」和「过期」是两件事:前者靠运行期可写的存储(shared dict 或 Redis),后者靠存储自带的 TTL。OpenResty 环境优先用 lua_shared_dict + exptime + 定时 flush_expired;已经在用 fail2ban 的,直接调 bantime 和 bantime.increment,成本最低。配完之后务必用 curl 从一台测试 IP 触发一次封禁,再等 TTL 到期确认自动解封,别只看日志。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2415.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~