配置Openresty为现有站点添加客户端设备访问限制

应用场景

  1. 业务代码零修改(前后端均不修改)
  2. 限制单一用户/设备在单位时间内访问特定接口的次数,防止机器人或脚本频繁刷新重要接口
  3. 限制单一用户/设备在单位时间内访问本站全部接口的总次数,防止机器人或爬虫过快访问
  4. 限制资源相关的接口在单位时间内,全站访问总次数,防止资源过载

运行原理

  1. 使用Openresty作为网关,代理客户端到业务服务器的所有请求
  2. 在Openresty上代理登录/授权接口,在该接口执行成功后,根据配置的密钥和客户端信息生成密文作为deviceId,通过Set-Cookie向客户端写入
  3. 在Openresty上定义deviceId校验方法,解密成功则通过
  4. 客户端的接口请求在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

参见

相关推荐
fajianchen18 小时前
什么是HTTP/2协议?NGINX如何支持HTTP/2并提升网站性能?
nginx·http
m0_7482487720 小时前
在 Ubuntu 上安装 Nginx 的详细指南
nginx·ubuntu·postgresql
若云止水1 天前
Ubuntu 下 nginx-1.24.0 源码分析 - ngx_os_init 函数
运维·nginx
m0_512744641 天前
Nginx(详解以及如何使用)
运维·服务器·nginx
铁锅与大鹅2 天前
http+nginx
网络协议·nginx·http
s_fox_2 天前
Nginx Embedded Variables 嵌入式变量解析(4)
java·网络·nginx
致奋斗的我们2 天前
Nginx反向代理及负载均衡
linux·运维·mysql·nginx·负载均衡·shell·openeluer
招风的黑耳2 天前
使用Nginx本地部署Axure生成的HTML文件,局域网内浏览器通过IP和地址访问
nginx·html·axure·本地部署
s_fox_2 天前
nginx ngx_http_module(7) 指令详解
运维·nginx·http
若云止水2 天前
Ubuntu 下 nginx-1.24.0 源码分析 - ngx_process_options
运维·nginx