Nginx 动态封 IP,60 行,给你一份生产级脚本(带避坑)
这是一个或许对你有用的社群 一对一交流/面试小册/简历优化/求职解惑欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料《项目实战视频》从书中学往事中“练”《互联网高频面试题》面朝简历学习春暖花开《架构 x 系统设计》摧枯拉朽掌控面试高频场景题《精进 Java 学习指南》系统学习互联网主流技术栈《必读 Java 源码专栏》知其然知其所以然这是一个或许对你有用的开源项目国产Star破10w的开源项目前端包括管理后台、微信小程序后端支持单体、微服务架构RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、ERP、CRM、AI大模型、IoT物联网等功能多模块https://gitee.com/zhijiantianya/ruoyi-vue-pro微服务https://gitee.com/zhijiantianya/yudao-cloud视频教程https://doc.iocoder.cn【国内首批】支持 JDK17/21SpringBoot3、JDK8/11Spring Boot2双版本凌晨三点的爬虫运维半小时 ban 一次 IP封 IP 在哪一层做三种方案决策矩阵接入 Nginx一行配置 access_by_lua_file生产级 Lua 脚本60 行能直接抄脚本里 4 个不能省的设计每一个都对应一个生产坑进阶白名单、动态阈值、监控告警总结60 行脚本能解决 80% 的爬虫问题凌晨三点的爬虫运维半小时 ban 一次 IP凌晨三点监控告警炸了QPS 飙到平时 10 倍网卡入向流量打满。一查源 IP——三个段在反复抓商品列表页明显是爬虫。运维大哥半睡半醒爬起来登服务器、改iptables、-D一段、reload——折腾半小时刚回床上躺下爬虫换段了又来一波。这就是绝大多数小团队封 IP 的真实状态手动、不及时、不分布式、不会自动解封。今天直接给一份能上生产的 60 行 Lua 脚本——Nginx 自动封 IP、时间到了自动解封、多机共享黑名单、Redis 抖动也不会拖死业务。环境前提LinuxCentOS 7 / Ubuntu 都行Redis 5.0Nginx 走 OpenResty自带 Lua 模块基于 Spring Boot MyBatis Plus Vue Element 实现的后台管理系统 用户小程序支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能项目地址https://github.com/YunaiV/ruoyi-vue-pro视频教程https://doc.iocoder.cn/video/封 IP 在哪一层做三种方案决策矩阵为什么选中间这套因为只有它同时满足动态和分布式——多台 Nginx 共用一份 Redis 黑名单封禁信息全集群一致时间到了自动解封不用手工清理。基于 Spring Cloud Alibaba Gateway Nacos RocketMQ Vue Element 实现的后台管理系统 用户小程序支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能项目地址https://github.com/YunaiV/yudao-cloud视频教程https://doc.iocoder.cn/video/接入 Nginx一行配置 access_by_lua_file在需要保护的 location 里加一行 Lua 钩子即可location / { # 如果有静态资源混在同一 location按需匹配 # if ($request_uri ~ .*\.(html|htm|jpg|js|css)) { # access_by_lua_file /usr/local/lua/access_limit.lua; # } access_by_lua_file /usr/local/lua/access_limit.lua; # access 阶段执行限流 alias /usr/local/web/; index index.html index.htm; }access_by_lua_file是 OpenResty 的 access 阶段钩子——每个进入这个 location 的请求都会先跑一遍 Lua 脚本。脚本判定通过才继续走 Nginx 后续流程。生产级 Lua 脚本60 行能直接抄-- /usr/local/lua/access_limit.lua -- 配置区建议外置到 nginx.conf 用 set_by_lua local pool_max_idle_time 10000 local pool_size 100 local redis_connection_timeout 100 local redis_host your redis host ip local redis_port 6379 local redis_auth your redis password local ip_block_time 120 -- 封禁时长秒 local ip_time_out 1 -- 限频窗口秒 local ip_max_count 3 -- 窗口内最大访问次数 -- 受信任的代理层数CDN LB Nginx 自己 视部署而定0 不信 X-Forwarded-For local trusted_proxy_hops 0 -- 工具函数 localfunction close_redis(red) ifnot red thenreturnend local ok, err red:set_keepalive(pool_max_idle_time, pool_size) ifnot ok then red:close() end end localfunction get_real_ip() if trusted_proxy_hops 0then return ngx.var.remote_addr end local xff ngx.req.get_headers()[X-Forwarded-For] ifnot xff thenreturn ngx.var.remote_addr end local ips {} for ip instring.gmatch(xff, ([^,])) do table.insert(ips, ip:match(^%s*(.-)%s*$)) end local target_idx #ips - trusted_proxy_hops return ips[target_idx] or ngx.var.remote_addr end -- 主流程fail-open 保护 local redis requireresty.redis local client redis:new() client:set_timeout(redis_connection_timeout) local ok, err client:connect(redis_host, redis_port) ifnot ok then -- Redis 不可用直接放行不阻断业务 ngx.log(ngx.WARN, redis connect failed, fail-open: , err) return end if client:get_reused_times() 0then local auth_ok, auth_err client:auth(redis_auth) ifnot auth_ok then ngx.log(ngx.ERR, redis auth failed: , auth_err) close_redis(client) return end end local clientIp get_real_ip() local incrKey limit:count: .. clientIp local blockKey limit:block: .. clientIp -- 命中黑名单 → 先归还连接再 exit local is_block client:get(blockKey) iftonumber(is_block) 1then close_redis(client) return ngx.exit(ngx.HTTP_FORBIDDEN) end -- EVAL 让 incr expire 原子化杜绝孤儿 key local count_script [[ local current redis.call(INCR, KEYS[1]) if current 1 then redis.call(EXPIRE, KEYS[1], ARGV[1]) end return current ]] local ip_count, eval_err client:eval(count_script, 1, incrKey, ip_time_out) ifnot ip_count then ngx.log(ngx.ERR, eval failed: , eval_err) close_redis(client) return end iftonumber(ip_count) ip_max_count then -- SET EX 一条命令无需两步 client:set(blockKey, 1, EX, ip_block_time) end close_redis(client)3 个变量按你业务调ip_max_count阈值/ip_time_out窗口/ip_block_time封禁时长。脚本里 4 个不能省的设计每一个都对应一个生产坑网上能搜到的版本基本都漏了下面 4 个点——少做一个就是一颗定时炸弹设计 1Redis 不可用 → fail-open 放行不是 500local ok, err client:connect(redis_host, redis_port) if not ok then ngx.log(ngx.WARN, redis connect failed, fail-open: , err) return -- ⭐ 不阻断 end踩过的坑很多脚本在 connect 失败时ngx.exit(500)阻断请求——Redis 抖一下整个网站全挂。安全机制不能反过来打死自己——爬虫多挡几秒不要紧正常用户挂了才是大事。设计 2close_redis调用必须在ngx.exit之前if tonumber(is_block) 1 then close_redis(client) -- ⭐ 先归还连接 return ngx.exit(ngx.HTTP_FORBIDDEN) end踩过的坑ngx.exit直接结束阶段后面代码完全不执行。如果close_redis写在ngx.exit后面每次 403 都泄露一个 Redis 连接——连接池迟早被打满。设计 3incr expire用 EVAL 原子化local count_script [[ local current redis.call(INCR, KEYS[1]) if current 1 then redis.call(EXPIRE, KEYS[1], ARGV[1]) end return current ]] local ip_count client:eval(count_script, 1, incrKey, ip_time_out)踩过的坑Lua 这层做incr然后再expire不是原子的——两条命令之间脚本被打断OOM、网络抖动下次再 INCR 时 key 没设 TTL这个 key 永远不会过期Redis 慢慢被孤儿 key 撑爆。设计 4默认用remote_addrX-Forwarded-For仅在信任代理链时启用if trusted_proxy_hops 0 then return ngx.var.remote_addr -- ⭐ TCP 层 IP伪造不了 end -- 信任链已知CDN → LB → Nginx才从右往左数第 N1 个踩过的坑直接X-Forwarded-For第一个值——爬虫伪造 header 来换 IP封禁形同虚设。这个 header 只能在你完全清楚信任链时使用否则用remote_addr才是底线。进阶白名单、动态阈值、监控告警白名单合作方 / 内部 IP 不走限流简单做法——在get_real_ip后加一段local whitelist { [192.168.0.0/16] true, [10.0.0.0/8] true, [1.2.3.4] true, -- 合作方固定 IP } -- 用 ngx.re.find 或 lua-resty-iputils 做 CIDR 匹配命中直接 return生产环境推荐用 lua-resty-iputils 做 CIDR 匹配性能比纯 Lua 字符串处理强一个数量级。动态阈值阈值放 Redis 里运维改完即生效把ip_max_count/ip_block_time从代码常量改成 Redis 读取——运维想调阈值不用 reload Nginxlocal ip_max_count tonumber(client:get(limit:config:max_count)) or 3 local ip_block_time tonumber(client:get(limit:config:block_time)) or 120配合一个简单的运维后台管理员调一下数字整个集群秒级生效。监控告警被封 IP 数量打到 Prometheus每次进入ngx.exit(403)时自增一个 metric counterPrometheus 抓回来画曲线。突然飙升 大概率攻击/爬虫高潮自动告警拉群。local metric_blocked ... -- 用 lua-resty-prometheus 注册的 counter metric_blocked:inc(1, {ip clientIp})总结60 行脚本能解决 80% 的爬虫问题回头看这套方案真正起决定作用的就三件事关键解决什么Redis 共享黑名单多台 Nginx 集群一致——一台拉黑、全集群生效TTL 自动过期不用人工解封时间到了自动释放fail-open 设计Redis 抖动不连累业务——安全机制不能打死自己最后说一句这套脚本能挡住 80% 的爬虫——剩下 20% 的高级对手带 IP 池、行为模拟需要更进一步的方案UA 指纹、行为分析、滑动验证码、TLS 指纹JA3……但先把基础挡住再去想高级的——别一上来就买商业 WAF对小团队是过度设计。欢迎加入我的知识星球全面提升技术能力。 加入方式“长按”或“扫描”下方二维码噢星球的内容包括项目实战、面试招聘、源码解析、学习路线。文章有帮助的话在看转发吧。 谢谢支持哟 (*^__^*