应用场景
- 业务代码零修改(前后端均不修改)
- 限制单一用户/设备在单位时间内访问特定接口的次数,防止机器人或脚本频繁刷新重要接口
- 限制单一用户/设备在单位时间内访问本站全部接口的总次数,防止机器人或爬虫过快访问
- 限制资源相关的接口在单位时间内,全站访问总次数,防止资源过载
运行原理
- 使用Openresty作为网关,代理客户端到业务服务器的所有请求
- 在Openresty上代理登录/授权接口,在该接口执行成功后,根据配置的密钥和客户端信息生成密文作为deviceId,通过Set-Cookie向客户端写入
- 在Openresty上定义deviceId校验方法,解密成功则通过
- 客户端的接口请求在Openresty上执行该接口匹配的location快内的lua代码,先校验deviceId,再根据设定的规则检查是否达到限流阈值,未达到限流阈值的通过proxy_pass透传给业务服务器执行,同时计数

安装配置
安装Openresty及组件
以centos为例
shell
yum install -y yum-utils
# <= CentOS 8
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
# >= CentOS 9
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
编辑nginx.conf配置初始化参数
shell
vi /usr/local/openresty/nginx/conf/nginx.conf
nginx.conf
http{
...
# 开启lua代码缓存
lua_code_cache on;
# 配置初始化参数
init_by_lua_block {
local drl = require("resty.device.ratelimit")
drl.config({
redis_uri = "redis://[:password@]host[:port][/database][?[timeout=timeout[d|h|m|s|ms|us|ns]]",
device_id_cookie_name = "deviceId",
server_device_check_urls = {
["your-website:80"] = "http://your-website/check-device-id"
}
})
}
# 复用nginx的配置文件
include /etc/nginx/conf.d/*.conf;
...
}
编辑站点配置文件
shell
vi /etc/nginx/conf.d/your_website.conf
代理登录/授权接口
your_website.conf
location /your-login-api {
access_by_lua_block {
local cjson = require("cjson")
local drl = require("resty.device.ratelimit")
local secret = "配置密钥"
-- 透传当前接口给后端服务器
local res = drl.proxy_pass("http://backend-server:8080")
if res.status ~= 200 then
ngx.say(res.body)
ngx.exit(res.status)
end
--假定登录接口返回json格式如下:
--{ "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")
-- 以明日23:59:59作为设备号过期时间
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
}
-- 将userId和明日日期加密作为devcieId,写入客户端cookie
local deviceId = drl.encrypt(cjson.encode(data), secret)
drl.set_response_cookie("deviceId", deviceId, tomorrow_end)
end
end
--返回登录/授权接口的response
ngx.say(res.body)
ngx.exit(res.status)
}
}
定义设备号校验接口
your_website.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 = "配置密钥"
-- 默认设备号无效
local response = {
valid = false,
expired_seconds = 1800
}
-- 解析Post Body的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
-- 解密deviceId,设置deviceId有效性,以及其Redis Key在距今x秒后过期
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
-- 返回deviceId是否有效及过期时间
ngx.header.content_type = 'application/json; charset=utf-8'
ngx.say(cjson.encode(response))
ngx.exit(200)
}
}
配置设备号接口访问限制
your_website.conf
# 根据需求配置各接口的限流规则
location /device-limit-apis/ {
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
-- 配置限流规则
-- 当前设备号最近3秒内(含)最多执行一次当前接口
-- 当前设备号最近10秒内(含)最多执行40次各类接口
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
-- 记录当前设备号当前接口当前秒执行1次
drl.record()
}
proxy_pass http://backend-server:8080;
}
配置全站接口访问限制
your_website.conf
location /print/ {
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
-- 最近10秒内(含)最多只允许当前接口执行4次
if drl.limit("global_current_uri", 10, 4) then
ngx.log(ngx.ERR, 'GLOBAL:', drl.device())
ngx.exit(503)
end
-- 记录当前设备号当前接口当前秒执行1次
drl.record()
}
proxy_pass http://backend-server:8080;
}
启动或reload
如已安装nginx,则先停用,openresty可替代nginx的全部功能
shell
systemctl stop nginx
systemctl disable nginx
启动
shell
systemctl start openresty