为了封禁某些爬虫或者恶意用户对服务器的请求,我们需要建立一个动态的IP黑名单。对于黑名单中的IP,将拒绝提供服务,并且可以设置封禁失效时间。
| 实现方式 | 优点 | 缺点 |
|---|---|---|
| iptables | 简单直接,物理层拦截 | 需要手动操作,不灵活 |
| Nginx+Lua+Redis | 动态封禁,分布式共享,自动失效 | 需学习Lua脚本 |
| 应用层代码 | 实现简单,易于维护 | 代码臃肿,高并发影响性能 |
选型结论:采用 Nginx + Lua + Redis 架构实现IP黑名单功能
|
1 2 3 |
客户端请求 → Nginx(Lua脚本) → Redis(检查黑名单/计数) → 后端服务 ↓ 封禁IP返回403 |
在需要进行限制的server的location中添加配置:
|
1 2 3 4 5 6 7 8 9 |
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; # 根据规则进行限流 alias /usr/local/web/; index index.html index.htm; } |
路径:/usr/local/lua/access_limit.lua
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
-- 自动将访问频次过高的IP地址加入黑名单封禁一段时间
-- ================= 配置参数 ================= -- 连接池超时回收(毫秒) local pool_max_idle_time = 10000 -- 连接池大小 local pool_size = 100 -- Redis连接超时时间(毫秒) local redis_connection_timeout = 100 -- Redis主机 local redis_host = "your_redis_host_ip" -- Redis端口 local redis_port = "6379" -- Redis认证密码 local redis_auth = "your_redis_password" -- 封禁IP时间(秒) local ip_block_time = 120 -- 指定IP访问频率时间段(秒) local ip_time_out = 1 -- 指定IP访问频率计数最大值(次) local ip_max_count = 3
-- ================= 工具函数 ================= -- 错误日志记录 local function errlog(msg, ex) ngx.log(ngx.ERR, msg, ex) end
-- 释放连接池 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 connct err:", err) return red:close() end 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
-- ================= 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)
-- 优化验证密码操作:代表连接在连接池使用的次数 -- 如果为0代表未使用,不为0代表复用,在只有为0时才进行密码校验 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
-- ================= 业务逻辑 ================= local cliendIp = getIp() local incrKey = "limit:count:" .. cliendIp local blockKey = "limit:block:" .. cliendIp
-- 查询IP是否被禁止访问,如果存在则返回403错误代码 local is_block, err = client:get(blockKey) if tonumber(is_block) == 1 then close_redis(client) ngx.exit(ngx.HTTP_FORBIDDEN) end
-- 增加访问计数 local ip_count, err = client:incr(incrKey) if tonumber(ip_count) == 1 then client:expire(incrKey, ip_time_out) end
-- 如果超过单位时间限制的访问次数,则添加限制访问标识 if tonumber(ip_count) > tonumber(ip_max_count) then client:set(blockKey, 1) client:expire(blockKey, ip_block_time) end
-- 释放Redis连接 close_redis(client) |
| Key格式 | 用途 | 过期时间 |
|---|---|---|
| limit:count:IP地址 | 记录IP在时间段内的访问次数 | ip_time_out(1秒) |
| limit:block:IP地址 | 封禁标识,值为1表示封禁 | ip_block_time(120秒) |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
开始 ↓ 获取客户端IP ↓ 连接Redis ↓ 检查blockKey是否存在? ├─ 是 → 返回403禁止访问 └─ 否 → 继续处理 ↓ 对incrKey执行incr操作 ↓ 如果是第一次访问,设置过期时间 ↓ 检查访问次数是否超过阈值? ├─ 是 → 设置blockKey封禁标识 └─ 否 → 正常访问 ↓ 释放Redis连接 ↓ 结束 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
-- 在检查黑名单之前添加白名单判断 local white_list = {"127.0.0.1", "192.168.1.*"}
local function isWhiteList(ip) for _, value in ipairs(white_list) do if value == ip or ngx.re.match(ip, value:gsub("%*", ".*")) then return true end end return false end
if isWhiteList(cliendIp) then close_redis(client) return -- 白名单直接放行 end |
对于频繁访问的IP,可以重定向到验证码页面:
|
1 2 3 4 5 6 7 8 9 10 |
if tonumber(ip_count) > tonumber(ip_max_count) then if tonumber(ip_count) < tonumber(ip_max_count) * 2 then -- 第一次超限,要求验证码 ngx.redirect("/captcha.html") else -- 多次超限,直接封禁 client:set(blockKey, 1) client:expire(blockKey, ip_block_time) end end |
结合访问日志分析,可实现更智能的封禁策略: