上周六晚上,我在公司机房值班,结果就碰到一个特别真实的场景:某个接口被刷了,访问量暴涨,服务器 CPU 飙升,Nginx access log 一看,全是来自几个相同的 IP 段。那一瞬间我就想,要是平时没准备好,手工去改 Nginx 配置然后 reload,效率太低,等你改完机器都快挂了。所以啊,今天就从我作为一个程序员经常折腾的角度,聊聊怎么在 Nginx 里实现动态封禁 IP。
静态黑名单的问题
先说传统的做法。我们大多数人应该都干过,在nginx.conf或某个server块里写:
deny 192.168.0.1;
deny 10.0.0.0/24;
然后nginx -s reload。这没毛病,简单粗暴,但是——致命点也明显:要 reload,影响所有连接。而且黑名单一多,配置臃肿不堪,改起来跟打补丁似的。
动态封禁的思路
我之前踩坑总结过,主要有几种办法:
依赖 fail2ban 这种外部工具,它通过扫描 Nginx 日志,匹配特定规则(比如 404 超过多少次),然后动态写 iptables 或 firewalld 来封禁。这种方法灵活,但依赖额外服务,调试麻烦。
利用 Nginx 自带的 ngx_http_limit_req_module + geo 模块,做一个内存级别的控制,把某些 IP 拉黑。这种好处是无感知、不需要 reload。
用 Lua(OpenResty)扩展 Nginx,把 IP 黑名单存到 Redis 之类的中间件里,随时可以写入和更新。Nginx 每次请求都先查 Redis,看是不是在黑名单。这是我自己线上最常用的一种方案。
Lua + Redis 方案举个例子
Nginx 配置里先引入 Lua 模块:
http {
lua_shared_dict ip_blacklist 10m;
init_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)
red:connect("127.0.0.1", 6379)
}
server {
location / {
access_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:connect("127.0.0.1", 6379)
local ip = ngx.var.remote_addr
local is_blocked = red:get("block:" .. ip)
if is_blocked == "1" then
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
}
proxy_pass http://backend;
}
}
}
这样一来,只要在 Redis 里插入set block:1.2.3.4 1,IP1.2.3.4立马被封掉,完全不用 reload,特别丝滑。
Java 代码管理黑名单
当然,光靠运维手动redis-cli不现实,我一般会写一个小服务,用 Java 暴露 HTTP 接口,方便动态加黑名单。比如 Spring Boot 起个简单的 Controller:
@RestController
@RequestMapping("/ip")
publicclass IpBlockController {
privatefinal StringRedisTemplate redisTemplate;
public IpBlockController(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@PostMapping("/block")
public String blockIp(@RequestParam String ip) {
redisTemplate.opsForValue().set("block:" + ip, "1", Duration.ofHours(1));
return"IP " + ip + " 已封禁";
}
@DeleteMapping("/unblock")
public String unblockIp(@RequestParam String ip) {
redisTemplate.delete("block:" + ip);
return"IP " + ip + " 已解封";
}
}
这样任何运维或者风控系统只要调一下接口,就能把 IP 封掉,不用去改 Nginx 配置,省事不少。
结合日志分析自动封禁
更高级一点,你甚至可以写个 Java 定时任务去扫 Nginx 日志,把短时间内请求超过阈值的 IP 自动丢进 Redis。这块我当时用过 logback 解析,也可以直接用 ELK。比如:
if (requestCount > 1000 && duration < 1 * 60) {
redisTemplate.opsForValue().set("block:" + ip, "1", Duration.ofMinutes(30));
}
这样就是一个自动封禁系统了,基本属于“轻量版 WAF”。
线上踩坑经历
我有一次就是用 fail2ban,结果规则写得太狠,把我们公司内网测试机全封了,害得测试同事一直登不上。后来换成 Redis 动态方案,配合白名单机制就稳了。还有一个坑是 Redis 挂掉怎么办?我加了本地 fallback,Nginx 启动时从 Redis 拉一份缓存进lua_shared_dict,即使 Redis 短暂挂掉,也不至于完全失效。
总结下
所以啊,在 Nginx 里搞动态封禁 IP,不是单纯写几行deny就完事。你要结合实际业务量,选择 fail2ban、ngx_lua、甚至是直接接入云厂商的防护方案。我的经验是:简单业务用静态规则,复杂业务用 Lua+Redis,关键场景上再叠加风控系统。