今天我们来聊聊如何通过 Nginx 实现一个动态的 IP 黑名单,阻止恶意用户或者爬虫访问你的服务器。
服务器正面临着越来越多不怀好意的访问请求,而你却希望能够优雅地应对——不直接拒绝所有流量,而是巧妙地在后台设置一个自动化的 IP 封禁机制。这一切,我们都可以通过结合 Nginx、Lua 和 Redis 来实现。
首先,我们得明白,封禁 IP 其实是我们防御爬虫、暴力破解、DDoS 攻击等恶意行为的一个重要手段。
很多人可能会问,为什么我们不直接使用操作系统层面的防火墙(如 iptables)来拦截这些 IP 呢?老实说,这样的方法虽然有效,但每次修改配置都得手动重启防火墙,操作起来比较繁琐。而且这种方式也比较死板,灵活性不高,特别是在处理动态变化的黑名单时,效果不够理想。
与之相比,基于 Nginx + Lua + Redis 的方案,提供了更加灵活和高效的方式,能够在 Web 服务器层面直接对请求进行控制,自动动态地更新和管理 IP 黑名单。而且,Redis 的引入,使得黑名单的数据可以在多台服务器之间共享,非常适合分布式环境。
在我们设计这个系统时,主要的工作是基于以下几点需求:
- 动态封禁:我们不希望每次封禁一个 IP 都需要手动修改配置,而是希望通过程序自动识别恶意行为并封禁 IP。
- 定时失效:我们要能控制封禁 IP 的时间,确保封禁过期后,IP 能够恢复访问。
- 性能:我们希望这个方案能在大规模并发的情况下,依然能够保证性能,不能对业务产生负面影响。
为了解决这些问题,我们决定采用 Nginx + Lua + Redis 的架构。具体来说,Nginx 会通过 Lua 脚本来处理每个请求,判断请求 IP 是否在 Redis 中的黑名单里。
如果是,就拒绝服务;如果不是,则继续正常响应。同时,Redis 用于存储封禁记录、访问频次等数据,提供跨服务器的共享机制。
首先,我们需要在 Nginx 的配置文件中引入 Lua 脚本。假设我们已经安装了 OpenResty 版本的 Nginx,它自带了 Lua 模块,可以方便地运行 Lua 代码。
在 Nginx 配置文件中,找到你需要添加限制的 location
配置,然后在其中加入 access_by_lua_file
指令,指向我们的 Lua 脚本文件。这个文件的路径可能是 /usr/local/lua/access_limit.lua
,根据你的实际路径调整。
location / {
access_by_lua_file /usr/local/lua/access_limit.lua; # 添加 Lua 脚本进行 IP 封禁
alias /usr/local/web/;
index index.html index.htm;
}
接下来,我们来写一下 Lua 脚本的核心部分。这个脚本的作用主要包括以下几个方面:
- 判断该 IP 的访问频次,如果超过阈值,就封禁该 IP。
-- 连接池参数配置
local pool_max_idle_time = 10000 -- 连接池超时回收时间,单位毫秒
local pool_size = 100 -- 连接池大小
local redis_connection_timeout = 100 -- Redis 连接超时时间
local redis_host = "your_redis_host" -- Redis 主机 IP
local redis_port = "your_redis_port" -- Redis 端口
local redis_auth = "your_redis_auth" -- Redis 密码(可选)
local ip_block_time = 120 -- 封禁时间(秒)
local ip_time_out = 1 -- 访问频率统计时间窗口(秒)
local ip_max_count = 3 -- 单位时间内允许的最大访问次数
-- 错误日志记录函数
local function errlog(msg, ex)
ngx.log(ngx.ERR, msg, ex)
end
-- 释放 Redis 连接
local function close_redis(red)
if not red then
return
end
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.say("redis connection error:", err)
return red:close()
end
end
-- 连接 Redis
local redis = require "resty.redis"
local client = redis:new()
local ok, err = client:connect(redis_host, redis_port)
if not ok then
close_redis(client)
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
client:set_timeout(redis_connection_timeout)
-- 如果是第一次连接 Redis,则需要进行认证
local connCount, err = client:get_reused_times()
if 0 == connCount then
local ok, err = client:auth(redis_auth)
if not ok then
errlog("failed to auth: ", err)
return
end
elseif err then
errlog("failed to get reused times: ", err)
return
end
-- 获取客户端的 IP 地址
local function getIp()
local clientIP = ngx.req.get_headers()["X-Real-IP"]
if clientIP == nil then
clientIP = ngx.req.get_headers()["x_forwarded_for"]
end
if clientIP == nil then
clientIP = ngx.var.remote_addr
end
return clientIP
end
local clientIp = getIp()
-- Redis 键值:IP 访问计数与封禁标记
local incrKey = "limit:count:"..clientIp
local blockKey = "limit:block:"..clientIp
-- 检查 IP 是否被封禁
local is_block, err = client:get(blockKey)
if tonumber(is_block) == 1 then
ngx.exit(ngx.HTTP_FORBIDDEN) -- 封禁时返回 403 错误
close_redis(client)
end
-- 统计访问频次
local ip_count, err = client:incr(incrKey)
if tonumber(ip_count) == 1 then
client:expire(incrKey, ip_time_out) -- 设置过期时间
end
-- 如果访问超过限制,则封禁该 IP
if tonumber(ip_count) > tonumber(ip_max_count) then
client:set(blockKey, 1)
client:expire(blockKey, ip_block_time) -- 设置封禁时间
end
close_redis(client)
解释代码
获取 IP 地址:我们通过 ngx.req.get_headers()
获取请求头中的 X-Real-IP
或 x_forwarded_for
字段,若这些字段不存在,则默认取 ngx.var.remote_addr
获取客户端的真实 IP。
连接 Redis:使用 resty.redis
模块连接 Redis 服务,所有 IP 黑名单信息都存储在 Redis 中。连接时,首先检查是否需要进行认证。
访问频次统计:通过 incr
指令统计每个 IP 的访问次数,并设置过期时间。若访问次数超过了预定的最大值(ip_max_count
),该 IP 将被加入封禁列表,不能继续访问。
封禁 IP:若 IP 被封禁,则返回 403 错误,阻止该 IP 继续访问。
通过上述配置和 Lua 脚本,我们就实现了一个灵活的动态封禁 IP 机制。Nginx 在前端处理请求,Lua 脚本在后端判断访问是否合规,而 Redis 则作为缓存和共享黑名单的存储介质。这个方案不仅配置简单,而且对性能影响较小,能够高效地阻挡恶意访问。
当你遇到爬虫频繁抓取数据、暴力破解密码,或者遭受 DDoS 攻击时,这个系统将帮你轻松应对。通过定制黑名单、自动封禁和失效时间等功能,你可以更好地保护你的服务器,免受恶意行为的侵扰。