配置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

参见

相关推荐
郝亚军11 小时前
使用Vue 3和Nginx打包和部署Vue.js项目的一般步骤
前端·vue.js·nginx
難釋懷20 小时前
Nginx代理https请求
redis·nginx·https
chexus2 天前
21. 深入 Nginx HTTP 缓存源码:CDN功能
nginx·http·缓存
BelongPanda3 天前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
郝亚军3 天前
nginx的三个基础库:PCRE、OpenSSL、Zlib的安装
linux·服务器·nginx
CodexDave3 天前
MySQL事务隔离级别与MVCC机制解析
前端·数据库·mysql·nginx·性能优化·负载均衡
chexus4 天前
20. 深入 Nginx 信号处理
linux·c语言·网络·nginx
名字还没想好☜5 天前
Nginx 反向代理与负载均衡配置实战
运维·nginx·负载均衡
砍材农夫5 天前
运维|devops|nginx|基本配置拆解
运维·nginx·devops