lua-resty-device-ratelimit
Non-invasive way to add client device access restrictions to an existing web app.
$ opm get sssxyd/lua-resty-device-ratelimit
Name
lua-resty-device-ratelimit: Using OpenResty, Non-invasive way to add client device access restrictions to an existing web app.
Requirements
Please follow the official documentation to install OpenResty Then, install the following modules:
openresty/lua-resty-redis
ledgetech/lua-resty-http
openresty/lua-resty-string
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 ledgetech/lua-resty-http
opm get openresty/lua-resty-string
opm get sssxyd/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
Please ensure that the DeviceId is unique.
Please ensure that the DeviceId you set is verifiable on the server.
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;
}
Authors
Yadong Xu
License
2bsd
Dependencies
Versions
-
Non-invasive way to add client device access restrictions to an existing web app. 2024-11-25 03:23:01
-
Non-invasive way to add client device access restrictions to an existing web app. 2023-12-29 02:40:15
-
Non-invasive way to add client device access restrictions to an existing web app. 2023-12-25 02:23:12
-
Add client interface access permissions and rate limits to your webapp. 2023-12-22 02:52:33