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

  • 相关阅读:
    [渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序实现继承
    [渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序处理并发
    [渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序使用异步及存储过程
    [渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序更新相关数据
    [渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序读取相关数据
    [渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序创建更复杂的数据模型
    [渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:MVC程序中实体框架的Code First迁移和部署
    [渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:MVC程序中实体框架的连接恢复和命令拦截
    centos常用命令集
    .NET Best Practices
  • 原文地址:https://www.cnblogs.com/reblue520/p/11457640.html
Copyright © 2020-2023  润新知