openresty开发系列37--nginx-lua-redis实现访问频率控制

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 适用就行了