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

参见

相关推荐
deming_su17 分钟前
轻松上手:使用Nginx实现高效负载均衡
运维·nginx·负载均衡
紫璨月5 小时前
nginx反向代理的bug
运维·nginx·bug
就叫飞六吧9 天前
基于keepalived、vip实现高可用nginx (centos)
python·nginx·centos
小生云木10 天前
Linux离线编译安装nginx
linux·运维·nginx
Cat God 00710 天前
项目上线(若依前后分离版)
java·nginx
婷儿z10 天前
LVS负载均衡群集:Nginx+Tomcat负载均衡群集
nginx·负载均衡·lvs
愿做无知一猿10 天前
【Docker】docker-compose中的nginx为何突然访问不到服务了?
nginx·docker·容器
当归102410 天前
Nginx与Tomcat:谁更适合你的服务器?
服务器·nginx·tomcat
yuren_xia10 天前
Nginx反向代理解决跨域问题详解
运维·nginx
骆驼Lara11 天前
前端跨域解决方案(6):Nginx
前端·javascript·nginx