openresty开发系列37--nginx-lua-redis实现访问频率控制
一)需求背景
在高并发场景下为了防止某个访问ip访问的频率过高,有时候会需要控制用户的访问频次
在openresty中,可以找到:
set_by_lua,rewrite_by_lua,access_by_lua,content_by_lua等方法。
那么访问控制应该是,access阶段。
我们用Nginx+Lua+Redis来做访问限制主要是考虑到高并发环境下快速访问控制的需求。
二)设计方案
我们用redis的key表示用户,value表示用户的请求频次,再利用过期时间实现单位时间;
现在我们要求10秒内只能访问10次frequency请求,超过返回403
1)首先为nginx.conf配置文件,nginx.conf部分内容如下:
location /frequency {
access_by_lua_file /usr/local/lua/access_by_limit_frequency.lua;
echo "访问成功";
}
2)编辑/usr/local/lua/access_by_limit_frequency.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_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 redis = require "resty.redis" --引入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) if not ok then close_redis(red) errlog("Cannot connect"); return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end local key = "limit:frequency:login:"..ngx.var.remote_addr --得到此客户端IP的频次 local resp, err = red:get(key) if not resp then close_redis(red) return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) --redis 获取值失败 end if resp == ngx.null then red:set(key, 1) -- 单位时间 第一次访问 red:expire(key, 10) --10秒时间 过期 end if type(resp) == "string" then if tonumber(resp) > 10 then -- 超过10次 close_redis(red) return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403 end end --调用API设置key ok, err = red:incr(key) if not ok then close_redis(red) return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) --redis 报错 end close_redis(red)
# 当redis设置了密码时,需要用red:auth() 方法进行验证
# vim /usr/local/lua/access_by_limit_frequency.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 function errlog(...) ngx.log(ngx.ERR, "redis: ", ...) 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) if not ok then close_redis(red) errlog("connot connect"); return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end 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 local key = "limit:frequency:login: " ..ngx.var.remote_addr --得到此客户端IP的频次 local resp,err = red:get(key) if not resp then close_redis(red) return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end if resp == ngx.null then red:set(key, 1) red:expire(key, 100) -- 设置过期时间,即返回403的时间为100秒 end --ngx.say("connect ok") --ngx.say("resp:",resp) if type(resp) == "string" then if tonumber(resp) > 10 then close_redis(red) return ngx.exit(ngx.HTTP_FORBIDDEN) end end ok, err = red:incr(key) if not ok then close_redis(red) return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end close_redis(red)
请求地址:/frequency
10秒内 超出10次 ,返回403
10秒后,又可以访问了
在Nginx需要限速的location中引用上述脚本配置示例:
location /user/ {
set $business "USER";
access_by_lua_file /usr/local/openresty/nginx/conf/lua/access.lua;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://user_224/user/;
}
注:对于有大量静态资源文件(如:js、css、图片等)的前端页面可以设置只有指定格式的请求才进行访问限速,示例代码如下:
location /h5 {
if ($request_uri ~ .*.(html|htm|jsp|json)) {
set $business "H5";
access_by_lua_file /usr/local/openresty/nginx/conf/lua/access.lua;
}
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://h5_224/h5;
}
如果我们想整个网站 都加上这个限制条件,那只要把
access_by_lua_file /usr/local/lua/access_by_limit_frequency.lua;
这个配置,放在server部分,让所有的location 适用就行了