通过 lua ,在 nginx 里结合 redis 进行请求频率等判断。防止请求进入服务层并压垮服务。
这里的判断逻辑很简单,判定是非法请求的依据有两点:
- 60 秒内请求达到 40 个
- 3600 秒内请求达到 500 个
相关IP及请求数都记在 redis 里。
nginx 配置
server {
listen 80;
server_name lua.test.cn;
root /home/httpd/sites/web;
location /lua {
default_type 'text/html';
lua_code_cache off;
access_by_lua_file /usr/local/openresty/nginx/conf/limits/access_block.lua;
}
}
防爬虫代码
access_block.lua
local config = require("common_config")
local redis = require("ngx_redis")
--封IP规则:一分钟访问超过 40次; 一小时访问超过 500 次
ip_block_time = 600 --封禁IP时间
ip_time_out = 60 --指定ip访问频率时间段
connect_count = 40 --指定ip访问频率计数最大值
ip_time_out_h = 3600
connect_count_h = 500
--连接redis
local cache = redis:create_connection()
if cache == 0 then
goto A
end
--白名单处理.如果IP在白名单内,直接放过
white_ip = {"100.97.", "123.125.71.", "42.236."}
is_white = "0"
for k, val in pairs(white_ip) do
local r = ngx.re.match(ngx.var.remote_addr, "^"..val..".*?$")
if r then
is_white = "1"
local ok, err = cache:close()
break;
end
end
if is_white == "0" then
--查询ip是否在封禁段内,若在则返回403错误代码
--因封禁时间会大于ip记录时间,故此处不对ip时间key和计数key做处理
is_block , err = cache:get("block_"..ngx.var.remote_addr)
if is_block == "1" then
ngx.exit(403)
goto A
end
start_time , err = cache:get("time_"..ngx.var.remote_addr)
ip_count , err = cache:get("count_"..ngx.var.remote_addr)
--如果ip记录时间大于指定时间间隔或者记录时间或者不存在ip时间key则重置时间key和计数key
--如果ip时间key小于时间间隔,则ip计数+1,且如果ip计数大于ip频率计数,则设置ip的封禁key为1
--同时设置封禁key的过期时间为封禁ip的时间
if start_time == ngx.null or os.time() - start_time > ip_time_out then
res , err = cache:set("time_"..ngx.var.remote_addr , os.time())
res , err = cache:set("count_"..ngx.var.remote_addr , 1)
else
ip_count = ip_count + 1
res , err = cache:incr("count_"..ngx.var.remote_addr)
if ip_count >= connect_count then
res , err = cache:set("block_"..ngx.var.remote_addr,1)
res , err = cache:expire("block_"..ngx.var.remote_addr,ip_block_time)
end
end
--查询ip是否在封禁段内,若在则返回403错误代码
--因封禁时间会大于ip记录时间,故此处不对ip时间key和计数key做处理
is_blockh_h , err_h = cache:get("block_h_"..ngx.var.remote_addr)
if is_block_h == "1" then
ngx.exit(403)
goto A
end
start_time_h , err_h = cache:get("time_h_"..ngx.var.remote_addr)
ip_count_h , err_h = cache:get("count_h_"..ngx.var.remote_addr)
--如果ip记录时间大于指定时间间隔或者记录时间或者不存在ip时间key则重置时间key和计数key
--如果ip时间key小于时间间隔,则ip计数+1,且如果ip计数大于ip频率计数,则设置ip的封禁key为1
--同时设置封禁key的过期时间为封禁ip的时间
if start_time_h == ngx.null or os.time() - start_time_h > ip_time_out_h then
res_h , err_h = cache:set("time_h_"..ngx.var.remote_addr , os.time())
res_h , err_h = cache:set("count_h_"..ngx.var.remote_addr , 1)
else
ip_count_h = ip_count_h + 1
res_h , err_h = cache:incr("count_h_"..ngx.var.remote_addr)
if ip_count_h >= connect_count_h then
res_h , err_h = cache:set("block_h_"..ngx.var.remote_addr,1)
res_h , err_h = cache:expire("block_h_"..ngx.var.remote_addr,ip_block_time)
end
end
end
--结尾标记
::A::
local ok, err = cache:close()
可以看出,代码里引用了两个其它的模块,common_config 和 ngx_redis。它们代码分别是:
全局配置代码
common_config.lua:
--全局配置
--局部变量.模块名称
local _M = {}
function _M.getRedisHost(self)
--return "100.92.113.11"
return "f2f163efe1ic11g4.m.cnhza.kvstore.aliyuncs.com"
end
function _M.getRedisPort(self)
return "6379"
end
function _M.getRedisPwd(self)
return "f2f163efe1ic11g4:yourpwd"
end
return _M
redis 连接池
ngx_redis.lua:
--Nginx-redis 封装组件.提供连接池之类的功能
local config = require("common_config")
local redis = require("resty.redis")
--局部变量.模块名称
local _M = {}
function _M.create_connection(self)
local host = config:getRedisHost()
local port = config:getRedisPort()
local pwd = config:getRedisPwd()
-- ngx.say(host, " : ", port)
--创建实例
local red = redis:new()
--设置超时(毫秒)
red:set_timeout(1000)
--建立连接
local ok, err = red:connect(host, port)
red:auth(pwd)
if not ok then
ngx.say("connect to redis error : ", err)
return 0
--return close_connection(red)
end
-- ngx.say("connect success!", host, port)
return red
end
function _M.close_connection(self, conn)
if not conn then
return
end
local ok, err = conn:close()
if not ok then
return 0
end
return 1
--释放连接(连接池实现)
--local pool_max_idle_time = 100000 --毫秒
--local pool_size = 100 --连接池大小
--local ok, err = conn:set_keepalive(pool_max_idle_time, pool_size)
--if not ok then
-- return 0
--end
--return 1
end
return _M
可以看到,这里自己做了连接池管理。它是以 resty.redis 为基础的。所以要先安装这个模块。