首页
学习
活动
专区
圈层
工具
发布

如何在Nginx 中实现动态封禁IP

上周六晚上,我在公司机房值班,结果就碰到一个特别真实的场景:某个接口被刷了,访问量暴涨,服务器 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,关键场景上再叠加风控系统

  • 发表于:
  • 原文链接https://page.om.qq.com/page/Olp-Hi-JONd4_BFS21sbLYrw0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券