Skip to content

Non-invasive way to add client device access restrictions to an existing web app

License

Notifications You must be signed in to change notification settings

sssxyd/lua-resty-device-ratelimit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

63 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Name

lua-resty-device-ratelimit: Using OpenResty, Non-invasive way to add client device access restrictions to an existing web app.

Table of Contents

Requirements

Please follow the official documentation to install OpenResty Then, install the following modules:

  1. openresty/lua-resty-redis
  2. pintsized/lua-resty-http
  3. openresty/lua-resty-string
  4. sssxyd/lua-resty-device-ratelimit

For CentOS, you can install them using the following commands:

yum install -y yum-utils

# For CentOS 8 or older
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
# For CentOS 9 or later
yum-config-manager --add-repo https://openresty.org/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

Non-Intrusive

Demo

Config

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"
            }
        })
    }

Proxy Your Login 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)
        }
    }

Create Validate DeviceId 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)
        }
    }

Check And Limit Your URIs

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;
    }

Intrusive

Demo

Config

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"
            }
        })
    }

Define The DeviceId

  1. Please ensure that the DeviceId is unique.
  2. Please ensure that the DeviceId you set is verifiable on the server.
  3. Please ensure that when the client sends a request, the deviceId is included in the request header.

Implement Validate DeviceId URI

Implement an interface to verify the validity of the deviceId. This interface should receive a JSON via POST and return a JSON

Received 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"
}

Response JSON

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

Check And Limit Your URIs

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;
    }

About

Non-invasive way to add client device access restrictions to an existing web app

Topics

Resources

License

Stars

Watchers

Forks

Packages