lua-resty-device-ratelimit

非侵入式地为现有 Web 应用添加客户端设备访问限制。

$ opm get sssxyd/lua-resty-device-ratelimit

名称

lua-resty-device-ratelimit: 使用 OpenResty,非侵入式地为现有 Web 应用添加客户端设备访问限制。

需求

请按照官方文档安装 OpenResty,然后安装以下模块

  1. openresty/lua-resty-redis

  2. pintsized/lua-resty-http

  3. openresty/lua-resty-string

  4. sssxyd/lua-resty-device-ratelimit

对于 CentOS,您可以使用以下命令安装它们

    yum install -y yum-utils
    
    # For CentOS 8 or older
    yum-config-manager --add-repo https://openresty.org.cn/package/centos/openresty.repo
    # For CentOS 9 or later
    yum-config-manager --add-repo https://openresty.org.cn/package/centos/openresty2.repo
    
    yum install -y openresty
    yum install -y openresty-opm openresty-resty
    
    opm get openresty/lua-resty-redis
    opm get pintsized/lua-resty-http
    opm get openresty/lua-resty-string
    opm get pintsized/lua-resty-device-ratelimit
    
    systemctl enable openresty
    

非侵入式

演示

配置

vim /usr/local/openresty/nginx/conf/nginx.conf redis_uri: redis :// [: password@] host [: port] [/ database][? [timeout=timeout[d|h|m|s|ms|us|ns]] server_device_check_urls: { ["server_name:listen_port"] = "your validate device uri for this site"}

        init_by_lua_block {
            local drl = require("resty.device.ratelimit")
            drl.config({
                redis_uri = "redis://:YourRedisPassword@127.0.0.1:6379/0",
                device_id_cookie_name = "CookieNameForDeviceId",
                server_device_check_urls = {
                    ["www.yoursite.com:80"] = "http://www.yoursite.com/check-device-id"
                }
            })
        }

代理您的登录 URI

vim /etc/nginx/conf.d/your-site.conf

        location /ajax/login {
            rewrite /ajax/(.*) /$1 break;
            access_by_lua_block {
                local cjson = require("cjson")
                local drl = require("resty.device.ratelimit")
                local secret = "Your_Secret_For_Encrypt"
    
                -- pass this uri to backend
                local res = drl.proxy_pass("http://backend-server:8080")
                
                if res.status ~= 200 then
                    ngx.say(res.body)
                    ngx.exit(res.status)
                end
    
                --Assume that your login interface returns a JSON format as follows:
                --{ "code":1, "message":"", "result":{"userId":156, ...} }
                local apiResponse = cjson.decode(res.body)
                if apiResponse and (tonumber(apiResponse.code) or 0) == 1 then
                    local result = apiResponse.result
                    if result and result.userId then
                        local now = os.date("*t") 
                        local tomorrow_end = os.time({year = now.year, month = now.month, day = now.day + 1, hour = 23, min = 59, sec = 59})
                        local data = {
                            userId = result.userId,
                            expired = tomorrow_end
                        }
                        -- encrypt userId and expiredTime as deviceId (hex format)
                        local deviceId = drl.encrypt(cjson.encode(data), secret)
                        drl.set_response_cookie("deviceId", deviceId, tomorrow_end)
                    end
                end
                
                ngx.say(res.body)
                ngx.exit(res.status)
            }
        }

创建验证设备 ID URI

vim /etc/nginx/conf.d/your-site.conf

        location /check-device-id {
            allow  127.0.0.1;
            deny  all;
    
            access_by_lua_block {
                local cjson = require("cjson")
                local drl = require("resty.device.ratelimit")
                local secret = "Your_Secret_For_Encrypt"
    
                -- default response body
                local response = {
                    valid = false,
                    expired_seconds = 1800
                }
    
                -- get deviceId from post json
                ngx.req.read_body()
                local body_data = ngx.req.get_body_data()
                local args, err
                if not body_data then
                    err = "failed to read request body"
                else
                    args, err = cjson.decode(body_data)
                end
                if not args then
                    ngx.log(ngx.ERR, "failed to decode JSON: ", err)
                    args = {}
                end
    
                -- decrypt deviceId and response 
                local encrypted_data_hex = args.device_id or ""
                if encrypted_data_hex ~= "" then
                    local datajson = drl.decrypt(encrypted_data_hex, secret)
                    if datajson then
                        local data = cjson.decode(datajson)
                        if data then
                            local expired = tonumber(data.expired) or 0
                            local expired_seconds = expired - os.time()
                            if expired_seconds < 0 then
                                response.valid = false
                                response.expired_seconds = 0
                            else
                                response.valid = true
                                response.expired_seconds = expired_seconds
                            end
                        end
                    end
                end
                
                ngx.header.content_type = 'application/json; charset=utf-8'
                ngx.say(cjson.encode(response))
                ngx.exit(200)
            }
        }

检查并限制您的 URI

vim /etc/nginx/conf.d/your-site.conf

        # no limit
        location /ajax/guest/ {
            rewrite /ajax/(.*) /$1 break;
            proxy_pass http://backend-server:8080;
        }
    
        # limit within the entire site, each uri to a maximum of 4 accesses within 10 seconds 
        location /ajax/io/ {
            access_by_lua_block {
                local drl = require("resty.device.ratelimit")
                -- 1. no deviceId or deviceId is invalid, exit with HTTP Status Code 401
                if not drl.check() then
                    ngx.log(ngx.ERR, 'AUTH:', drl.device())
                    ngx.exit(401)
                end
                -- 2. if access count of current uri >= 4 times in latest 10 seconds within the entire site 
                if drl.limit("global_current_uri", 10, 4) then
                    ngx.log(ngx.ERR, 'GLOBAL:', drl.device())
                    ngx.exit(503)
                end
                -- 3. asynchronously log this visit and continue execution 
                drl.record()
            }
            rewrite /ajax/(.*) /$1 break;
            proxy_pass http://backend-server:8080;
        }
    
        # Limit a single device to a maximum of 1 access per interface within 1 seconds
        location /ajax/key/ {
            access_by_lua_block {
                local drl = require("resty.device.ratelimit")
                -- no deviceId or deviceId is invalid, exit with HTTP Status Code 401
                if not drl.check() then
                    ngx.log(ngx.ERR, 'AUTH:', drl.device())
                    ngx.exit(401)
                end
                if drl.limit("device_current_uri", 1, 1) then
                    ngx.log(ngx.ERR, 'LIMIT:', drl.device())
                    ngx.exit(429)
                end
                -- asynchronously log this visit and continue execution 
                drl.record()
            }
            rewrite /ajax/(.*) /$1 break;
            proxy_pass http://backend-server:8080;
        }
    
        # Limit a single device to a maximum of 1 access per interface within 3 seconds, and a total of no more than 40 accesses across all interfaces within 10 seconds
        location /ajax/ {
            access_by_lua_block {
                local drl = require("resty.device.ratelimit")
                if not drl.check() then
                    ngx.log(ngx.ERR, 'AUTH:', drl.device())
                    ngx.exit(401)
                end
                if drl.limit("device_current_uri", 3, 1) or drl.limit("device_total_uris", 10, 40) then
                    ngx.log(ngx.ERR, 'LIMIT:', drl.device())
                    ngx.exit(429)
                end
                drl.record()
            }
            rewrite /ajax/(.*) /$1 break;
            proxy_pass http://backend-server:8080;
        }
    
        location / {
            try_files $uri  $uri/ /index.html;
        }

侵入式

演示

配置

vim /usr/local/openresty/nginx/conf/nginx.conf redis_uri: redis :// [: password@] host [: port] [/ database][? [timeout=timeout[d|h|m|s|ms|us|ns]] server_device_check_urls: { ["server_name:listen_port"] = "your validate device uri for this site"}

        init_by_lua_block {
            local drl = require("resty.device.ratelimit")
            drl.config({
                redis_uri = "redis://:YourRedisPassword@127.0.0.1:6379/0",
                device_id_header_name = "x-device-id",
                server_device_check_urls = {
                    ["www.yoursite.com:80"] = "http://backend-server:8080/your-validate-device-id-api"
                }
            })
        }

定义设备 ID

  1. 请确保设备 ID 是唯一的。

  2. 请确保您设置的设备 ID 在服务器上是可以验证的。

  3. 请确保客户端发送请求时,请求头中包含 deviceId。

实现验证设备 ID URI

实现一个接口来验证 deviceId 的有效性。此接口应通过 POST 接收 JSON 并返回 JSON

接收到的 JSON

    {
    "device_id": "device id",
    "remote_addr": "client ip",
    "request_uri": "request uri",
    "request_time": "unix timestamp",
    "request_headers": {"x-device-id":"your device id", "other-header":""},
    "server_name": "server_name defined in server block",
    "server_port": "listening port defined in server block"
    }

响应 JSON

    {
        "valid": true,
        "expired": 3600
    }

检查并限制您的 URI

vim /etc/nginx/conf.d/your-site.conf

        # no limit
        location /ajax/guest/ {
            rewrite /ajax/(.*) /$1 break;
            proxy_pass http://backend-server:8080;
        }
    
        # limit within the entire site, each uri to a maximum of 4 accesses within 10 seconds 
        location /ajax/io/ {
            access_by_lua_block {
                local drl = require("resty.device.ratelimit")
                -- 1. no deviceId or deviceId is invalid, exit with HTTP Status Code 401
                if not drl.check() then
                    ngx.log(ngx.ERR, 'AUTH:', drl.device())
                    ngx.exit(401)
                end
                -- 2. if access count of current uri >= 4 times in latest 10 seconds within the entire site 
                if drl.limit("global_current_uri", 10, 4) then
                    ngx.log(ngx.ERR, 'GLOBAL:', drl.device())
                    ngx.exit(503)
                end
                -- 3. asynchronously log this visit and continue execution 
                drl.record()
            }
            rewrite /ajax/(.*) /$1 break;
            proxy_pass http://backend-server:8080;
        }
    
        # Limit a single device to a maximum of 1 access per interface within 1 seconds
        location /ajax/key/ {
            access_by_lua_block {
                local drl = require("resty.device.ratelimit")
                if not drl.check() then
                    ngx.log(ngx.ERR, 'AUTH:', drl.device())
                    ngx.exit(401)
                end
                if drl.limit("device_current_uri", 1, 1) then
                    ngx.log(ngx.ERR, 'LIMIT:', drl.device())
                    ngx.exit(429)
                end
                drl.record()
            }
            rewrite /ajax/(.*) /$1 break;
            proxy_pass http://backend-server:8080;
        }
    
        # Limit a single device to a maximum of 1 access per interface within 3 seconds, and a total of no more than 40 accesses across all interfaces within 10 seconds
        location /ajax/ {
            access_by_lua_block {
                local drl = require("resty.device.ratelimit")
                if not drl.check() then
                    ngx.log(ngx.ERR, 'AUTH:', drl.device())
                    ngx.exit(401)
                end
                if drl.limit("device_current_uri", 3, 1) or drl.limit("device_total_uris", 10, 40) then
                    ngx.log(ngx.ERR, 'LIMIT:', drl.device())
                    ngx.exit(429)
                end
                drl.record()
            }
            rewrite /ajax/(.*) /$1 break;
            proxy_pass http://backend-server:8080;
        }
    
        location / {
            try_files $uri  $uri/ /index.html;
        }

作者

徐亚东

许可证

2bsd

依赖项

版本