一)需求背景 为了封禁某些爬虫或者恶意用户对服务器的请求,我们需要建立一个动态的 IP 黑名单。 对于黑名单之内的 IP ,拒绝提供服务。 二)设计方案 实现 IP 黑名单的功能有很多途径: 1、在操作系统层面,配置 iptables,拒绝指定 IP 的网络请求; 2、在 Web Server 层面,通过 Nginx 自身的 deny 选项 或者 lua 插件 配置 IP 黑名单; 3、在应用层面,在请求服务之前检查一遍客户端 IP 是否在黑名单。 为了方便管理和共享,我们通过 Nginx+Lua+Redis 的架构实现 IP 黑名单的功能 如图 配置nginx.conf 在http部分,配置本地缓存,来缓存redis中的数据,避免每次都请求redis lua_shared_dict shared_ip_blacklist 1m; #定义ip_blacklist 本地缓存变量 location /ipblacklist { access_by_lua_file /usr/local/lua/access_by_limit_ip.lua; echo "ipblacklist"; } local function close_redis(red) if not red then return end --释放连接(连接池实现) local pool_max_idle_time = 10000 --毫秒 local pool_size = 100 --连接池大小 local ok, err = red:set_keepalive(pool_max_idle_time, pool_size) if not ok then ngx.say("set keepalive error : ", err) end end local function errlog(...) ngx.log(ngx.ERR, "redis: ", ...) end local function duglog(...) ngx.log(ngx.DEBUG, "redis: ", ...) end local function getIp() local myIP = ngx.req.get_headers()["X-Real-IP"] if myIP == nil then myIP = ngx.req.get_headers()["x_forwarded_for"] end if myIP == nil then myIP = ngx.var.remote_addr end return myIP; end local key = "limit:ip:blacklist" local ip = getIp(); local shared_ip_blacklist = ngx.shared.shared_ip_blacklist --获得本地缓存的最新刷新时间 local last_update_time = shared_ip_blacklist:get("last_update_time"); if last_update_time ~= nil then local dif_time = ngx.now() - last_update_time if dif_time < 60 then --缓存1分钟,没有过期 if shared_ip_blacklist:get(ip) then return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403 end return end end local redis = require "resty.redis" --引入redis模块 local red = redis:new() --创建一个对象,注意是用冒号调用的 --设置超时(毫秒) red:set_timeout(1000) --建立连接 local ip = "192.168.5.202" local port = 6379 local ok, err = red:connect(ip, port) if not ok then close_redis(red) errlog("limit ip cannot connect redis"); else local ip_blacklist, err = red:smembers(key); if err then errlog("limit ip smembers"); else --刷新本地缓存,重新设置 shared_ip_blacklist:flush_all(); --同步redis黑名单 到 本地缓存 for i,bip in ipairs(ip_blacklist) do --本地缓存redis中的黑名单 shared_ip_blacklist:set(bip,true); end --设置本地缓存的最新更新时间 shared_ip_blacklist:set("last_update_time",ngx.now()); end end if shared_ip_blacklist:get(ip) then return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403 end ############################################### ############################################### ############################################### 当redis设置了密码时代码如下: [root@node5 lua]# cat /usr/local/lua/access_by_limit_ip.lua local function close_reis(red) if not red then return end local pool_max_idle_time = 10000 local pool_size = 100 local ok, err = red:set_keepalive(pool_max_idle_time, pool_size) if not ok then ngx.say("set keepalive error :", err) end end local function errlog(...) ngx.log(ngx.ERR, "redis: ", ...) end local function duglog(...) ngx.log(ngx.DEBUG, "redis: ",...) end local function getIp() local myip = ngx.req.get_headers()["X-Real-IP"] if myip == nil then myip = ngx.req.get_headers()["x_forwarded_for"] end if myip == nil then myip = ngx.var.remote_addr end return myip end local key = "limit:ip:blacklist" local ip = getIp(); local shared_ip_blacklist = ngx.shared.shared_ip_blacklist local last_update_time = shared_ip_blacklist:get("last_update_time"); if last_update_time ~= nil then local dif_time = ngx.now() - last_update_time if dif_time < 60 then if shared_ip_blacklist:get(ip) then return ngx.exit(ngx.HTTP_FORBIDDEN) end return end end local redis = require "resty.redis" local red = redis:new() red:set_timeout(1000) local ip = "10.11.0.215" local port = 6379 local ok, err = red:connect(ip,port) local count, err = red:get_reused_times() if 0 == count then ----新建连接,需要认证密码 ok, err = red:auth("redis123") if not ok then ngx.say("failed to auth: ", err) return end elseif err then ----从连接池中获取连接,无需再次认证密码 ngx.say("failed to get reused times: ", err) return end if not ok then close_redis(red) errlog("limit ip cannot connect redis"); else local ip_blacklist, err = red:smembers(key) if err then errlog("limit ip smembers") else shared_ip_blacklist:flush_all(); for i,bip in ipairs(ip_blacklist) do shared_ip_blacklist:set(bip, true); end shared_ip_blacklist:set("last_update_time", ngx.now()); end end if shared_ip_blacklist:get(ip) then return ngx.exit(ngx.HTTP_FORBIDDEN) end 用户redis客户端设置: 添加黑名单IP: sadd limit:ip:blacklist 10.11.0.148 获取黑名单IP: smembers limit:ip:blacklist 10.11.0.215:6379> sadd limit:ip:blacklist 10.11.0.148 10.11.0.215:6379> sadd limit:ip:blacklist 10.11.0.215 10.11.0.215:6379> smembers limit:ip:blacklist 1) "10.11.0.215" 2) "10.11.0.148" 10.11.0.215:6379> smembers limit:ip:blacklist 1) "10.11.0.215" 2) "10.11.0.148" 此方法目前只能实现手动添加黑名单IP进行IP封禁,在某些场景如:半夜如果有人恶意爬取网站服务器可能导致服务器资源耗尽崩溃或者影响业务 下面是改进后的代码,可以实现自动将访问频次过高的IP地址加入黑名单封禁一段时间 nginx.conf配置部分: location /goodslist { set $business "USER"; access_by_lua_file /usr/local/lua/access_count_limit.lua; echo "get goods list success"; } lua代码: [root@node5 lua]# cat /usr/local/luaaccess_count_limit.lua local function close_redis(red) if not red then return end local pool_max_idle_time = 10000 local pool_size = 100 local ok, err = red:set_keepalive(pool_max_idle_tme, pool_size) if not ok then ngx.say("set keepalive err : ", err) end end local ip_block_time=300 --封禁IP时间(秒) local ip_time_out=30 --指定ip访问频率时间段(秒) local ip_max_count=20 --指定ip访问频率计数最大值(秒) local BUSINESS = ngx.var.business --nginx的location中定义的业务标识符 --连接redis local redis = require "resty.redis" local conn = redis:new() ok, err = conn:connect("10.11.0.215", 6379) conn:set_timeout(2000) --超时时间2秒 --如果连接失败,跳转到脚本结尾 if not ok then --goto FLAG close_redis(conn) end local count, err = conn:get_reused_times() if 0 == count then ----新建连接,需要认证密码 ok, err = conn:auth("redis123") if not ok then ngx.say("failed to auth: ", err) return end elseif err then ----从连接池中获取连接,无需再次认证密码 ngx.say("failed to get reused times: ", err) return end --查询ip是否被禁止访问,如果存在则返回403错误代码 is_block, err = conn:get(BUSINESS.."-BLOCK-"..ngx.var.remote_addr) if is_block == '1' then ngx.exit(403) close_redis(conn) end --查询redis中保存的ip的计数器 ip_count, err = conn:get(BUSINESS.."-COUNT-"..ngx.var.remote_addr) if ip_count == ngx.null then --如果不存在,则将该IP存入redis,并将计数器设置为1、该KEY的超时时间为ip_time_out res, err = conn:set(BUSINESS.."-COUNT-"..ngx.var.remote_addr, 1) res, err = conn:expire(BUSINESS.."-COUNT-"..ngx.var.remote_addr, ip_time_out) else ip_count = ip_count + 1 --存在则将单位时间内的访问次数加1 if ip_count >= ip_max_count then --如果超过单位时间限制的访问次数,则添加限制访问标识,限制时间为ip_block_time res, err = conn:set(BUSINESS.."-BLOCK-"..ngx.var.remote_addr, 1) res, err = conn:expire(BUSINESS.."-BLOCK-"..ngx.var.remote_addr, ip_block_time) else res, err = conn:set(BUSINESS.."-COUNT-"..ngx.var.remote_addr,ip_count) res, err = conn:expire(BUSINESS.."-COUNT-"..ngx.var.remote_addr, ip_time_out) end end -- 结束标记 local ok, err = conn:close() # redis的数据 10.11.0.215:6379> get USER-COUNT-10.11.0.148 "16" 10.11.0.215:6379> get USER-BLOCK-10.11.0.148 (nil) 四、总结 以上,便是 Nginx+Lua+Redis 实现的 IP 黑名单功能,具有如下优点: 1、配置简单、轻量,几乎对服务器性能不产生影响; 2、多台服务器可以通过Redis实例共享黑名单; 3、动态配置,可以手工或者通过某种自动化的方式设置 Redis 中的黑名单。