21 10 月 2026
本文由财富云计算提供,我们提供高防服务器与 CDN 防护服务,NAT 型无独立公网 IP 价格比独立 IP 省一半以上,源站自研 WAF 是我们的高防方案里源站加固的标配,见文末。

一、为什么需要 Lua 层 WAF

Nginx 配置防护的天花板:

需求Nginx 配置Lua
静态频率限制✅ rate 参数✅
按接口不同阈值⚠️ 每 location 写死✅ 代码动态判断
检查 POST 请求体❌ 做不到✅ 逐字节检查
多维度关联(IP+UA+路径)❌✅
全局黑名单(多机同步)❌✅ Redis 共享
滑动窗口限流❌(漏桶)✅ 精确实现
学习正常流量基线❌✅

OpenResty = Nginx + LuaJIT,Lua 跑在请求处理的各个阶段,性能和 C 模块接近——防护逻辑零成本嵌入。

二、安装 OpenResty

# Ubuntu/Debian
apt install -y curl gnupg2
curl -fsSL https://openresty.org/package/pubkey.gpg | gpg --dearmor -o /usr/share/keyrings/openresty.gpg
echo "deb [signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/openresty.list
apt update && apt install -y openresty

# CentOS/RHEL
yum install -y yum-utils
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
yum install -y openresty

# 验证
/usr/local/openresty/bin/openresty -v
# 显示 nginx version: openresty/x.x.x = 成功
# 设为系统服务(用 systemd 管理,不是原版 nginx 命令)
# openresty 自带 service 文件,或者软链:
ln -sf /usr/local/openresty/bin/openresty /usr/local/bin/nginx  # 这样能用 nginx 命令
nginx -v && systemctl enable openresty

目录说明:

  • 配置:/usr/local/openresty/nginx/conf/nginx.conf
  • Lua 代码:/usr/local/openresty/nginx/lua/(自建)
  • 日志:/usr/local/openresty/nginx/logs/

三、WAF 第一层:访问日志与 IP 还原

CDN/高防场景,先拿真实 IP,否则后面全错:

# nginx.conf http 块
http {
    # 信任的回源段(换成你的 CDN/高防 IP 段)
    set_real_ip_from 203.0.113.0/24;
    set_real_ip_from 198.51.100.0/24;
    real_ip_header X-Forwarded-For;
    real_ip_recursive on;
    
    lua_shared_dict waf_cache 64m;      # 共享内存(计数、黑名单缓存)
    lua_code_cache on;                  # 生产开缓存(改 Lua 要 reload)
    
    resolver 223.5.5.5 valid=300 ipv6=off;  # 内部用
    
    init_by_lua_block {
        -- 全局初始化(加载模块、读配置)
        waf = require "waf.init"
        waf.init()
    }
}

四、WAF 核心:动态限流模块

比 Nginx rate 更强:滑动窗口 + 按维度 + 动态阈值。

-- /usr/local/openresty/nginx/lua/limit.lua
local _M = {}

local dict = ngx.shared.waf_cache

-- 滑动窗口限流
-- key: 限流键(如 "ip:1.2.3.4:/api/search")
-- limit: 窗口内最大次数  window: 窗口秒数
function _M.sliding_window(key, limit, window)
    local now = ngx.time()
    -- 用有序结构太复杂,简化为「分桶滑动」
    local bucket = math.floor(now / window)
    local rk = key .. ":" .. bucket
    local cnt, err = dict:incr(rk, 1, 0)
    if not cnt then
        ngx.log(ngx.ERR, "limit incr failed: ", err)
        return true  -- 出错时放行(fail-open,宁误放不误杀)
    end
    -- 设置过期(两个窗口,保证滑动边界)
    dict:expire(rk, window * 2)
    
    if cnt > limit then
        return false
    end
    return true
end

-- 按 IP + URI 分级限流
function _M.check_request()
    local ip = ngx.var.remote_addr
    local uri = ngx.var.uri
    
    -- 1. 全局 IP 限制:单 IP 全站 200 次/分钟
    if not _M.sliding_window("ip:" .. ip, 200, 60) then
        return "global_ip_limit"
    end
    
    -- 2. 贵接口单独限:/api/search 10 次/分钟
    if uri:match("^/api/search") or uri:match("^/api/query") then
        if not _M.sliding_window("search:" .. ip, 10, 60) then
            return "search_limit"
        end
    end
    
    -- 3. 登录接口:5 次/分钟(防爆破)
    if uri == "/login" or uri == "/api/login" then
        if not _M.sliding_window("login:" .. ip, 5, 60) then
            return "login_limit"
        end
    end
    
    -- 4. 短信/邮件接口:1 次/分钟
    if uri:match("/sms/") or uri:match("/email/") then
        if not _M.sliding_window("sms:" .. ip, 1, 60) then
            return "sms_limit"
        end
    end
    
    return nil  -- 通过
end

return _M

用法(在 access 阶段调用):

server {
    location / {
        access_by_lua_block {
            local limit = require "limit"
            local hit = limit.check_request()
            if hit then
                ngx.log(ngx.WARN, "rate limited: ", hit, " ip=", ngx.var.remote_addr)
                ngx.status = 429
                ngx.header["Retry-After"] = 60
                ngx.say('{"code":429,"msg":"请求过于频繁,请稍后再试"}')
                return ngx.exit(429)
            end
        }
        proxy_pass http://backend;
    }
}

五、WAF 第二层:攻击特征检测

5.1 请求体内容检查(Nginx 配置做不到的)

-- /usr/local/openresty/nginx/lua/inspect.lua
local _M = {}

-- SQL 注入特征
local sql_patterns = {
    "union%s+select", "select%s+.*%s+from", "insert%s+into", "drop%s+table",
    "'%s+or%s+'", "or%s+1%s*=%s*1", "sleep%s*%(", "benchmark%s*%(",
    "information_schema", "xp_cmdshell", "%/\\*.+\\*\\/",
}

-- XSS 特征
local xss_patterns = {
    "<script", "javascript:", "onerror%s*=", "onload%s*=",
    "<iframe", "document%.cookie", "eval%s*%(",
}

-- 路径遍历
local traversal_patterns = {
    "%.%.%/", "%.%.\\\\", "/etc/passwd", "/proc/self",
    "boot%.ini", "win%.ini",
}

local function check_any(str, patterns)
    if not str then return false end
    local lower = str:lower()
    for _, p in ipairs(patterns) do
        if lower:find(p) then
            return true
        end
    end
    return false
end

-- 检查 URI + 参数 + POST body
function _M.check_payload()
    local uri = ngx.var.request_uri
    
    -- 1. URI 和查询参数
    if check_any(uri, sql_patterns) then return "sqli" end
    if check_any(uri, xss_patterns) then return "xss" end
    if check_any(uri, traversal_patterns) then return "traversal" end
    
    -- 2. POST body(按需开启,读 body 有开销)
    if ngx.req.get_method() == "POST" then
        ngx.req.read_body()
        local body = ngx.req.get_body_data()
        if body and #body > 0 then
            if check_any(body, sql_patterns) then return "sqli_body" end
            if check_any(body, xss_patterns) then return "xss_body" end
            -- 大 body 采样检查(性能优化)
            if #body > 102400 then
                local sample = body:sub(1, 102400)
                if check_any(sample, sql_patterns) then return "sqli_body" end
            end
        end
    end
    
    return nil
end

return _M

5.2 接入 WAF 主流程

-- /usr/local/openresty/nginx/lua/waf/init.lua
local limit = require "limit"
local inspect = require "inspect"
local _M = {}

function _M.init()
    ngx.log(ngx.NOTICE, "WAF initialized")
end

-- 每个请求的入口
function _M.access()
    local ip = ngx.var.remote_addr
    
    -- 1. 黑名单(Redis 联动,见第六节)
    local blocked = require "blocklist"
    if blocked.check(ip) then
        return ngx.exit(403)
    end
    
    -- 2. 频率限制
    local hit = limit.check_request()
    if hit then
        return ngx.exit(429)
    end
    
    -- 3. 攻击特征
    local attack = inspect.check_payload()
    if attack then
        ngx.log(ngx.WARN, "attack detected: ", attack, " ip=", ip, " uri=", ngx.var.request_uri)
        -- 命中特征的 IP 记一次「违规」,累计进黑名单
        blocked.strike(ip)
        return ngx.exit(403)
    end
end

return _M
server {
    location / {
        access_by_lua_block {
            require("waf.init").access()
        }
        proxy_pass http://backend;
    }
}

六、WAF 第三层:Redis 全局黑名单

多台服务器共享黑名单、违规计分自动封禁:

-- /usr/local/openresty/nginx/lua/blocklist.lua
local redis = require "resty.redis"
local _M = {}

local REDIS_HOST = "127.0.0.1"
local REDIS_PORT = 6379

local function connect()
    local red = redis:new()
    red:set_timeout(200)
    local ok, err = red:connect(REDIS_HOST, REDIS_PORT)
    if not ok then
        ngx.log(ngx.ERR, "redis connect failed: ", err)
        return nil
    end
    return red
end

-- 检查 IP 是否被封
function _M.check(ip)
    -- 先查本地共享内存(快),没有再查 Redis
    local cached = ngx.shared.waf_cache:get("blocked:" .. ip)
    if cached then return true end
    
    local red = connect()
    if not red then
        return false  -- Redis 挂了放行(或按策略 fail-close)
    end
    local res, err = red:get("waf:blocked:" .. ip)
    red:set_keepalive(10000, 100)
    
    if res and res ~= ngx.null then
        ngx.shared.waf_cache:set("blocked:" .. ip, true, 60)  -- 本地缓存 60 秒
        return true
    end
    return false
end

-- 违规计分:3 次违规封 1 小时
function _M.strike(ip)
    local red = connect()
    if not red then return end
    
    local key = "waf:strikes:" .. ip
    local n, err = red:incr(key)
    red:expire(key, 3600)
    
    if n and n >= 3 then
        red:set("waf:blocked:" .. ip, "1")
        red:expire("waf:blocked:" .. ip, 3600)
        ngx.log(ngx.WARN, "IP banned for 1h: ", ip, " strikes=", n)
    end
    red:set_keepalive(10000, 100)
end

return _M
# 装 Redis
apt install -y redis-server
systemctl enable --now redis-server

# 手动封禁/解封
redis-cli set waf:blocked:1.2.3.4 1 EX 3600
redis-cli del waf:blocked:1.2.3.4
redis-cli keys "waf:strikes:*" | head   # 看违规记录

七、性能与调优

Lua WAF 虽好,别把机器跑垮:

7.1 分阶段控制开销

server {
    # 静态资源不做 payload 检查(检查也白检查,攻击者不打静态)
    location ~* \.(jpg|css|js|png|woff2?)$ {
        access_log off;
        expires 30d;
    }
    
    # 动态请求全量 WAF
    location / {
        access_by_lua_block { require("waf.init").access() }
        proxy_pass http://backend;
    }
}

7.2 共享内存调优

lua_shared_dict waf_cache 128m;   # 黑名单缓存 + 计数,按 QPS 调

7.3 Lua 代码缓存

lua_code_cache on;    # 生产必须 on(改代码后 reload 生效)
# 开发调试可临时 off(每次请求重新编译,极慢)

7.4 性能基准参考

2 核 4G 机器跑上述 WAF:单请求增加 0.1–0.3ms、QPS 上限比裸 Nginx 降 10–20%——换来的防护能力值这个价。追求极限性能可以把特征检测放 CDN/WAF 产品层,源站 Lua 只做兜底限流。

八、部署检查清单

□ openresty -t 通过(语法)
□ 测试正常请求全部放行(没被规则误杀)
□ 测试 limit:for i in $(seq 1 300); do curl -s -o /dev/null https://域名/; done → 后段 429
□ 测试 SQLi:curl "https://域名/?id=1' union select 1,2,3--" → 403
□ 测试 XSS:curl "https://域名/?q=<script>alert(1)</script>" → 403
□ Redis 联通:redis-cli ping → PONG
□ 黑名单生效:redis-cli set waf:blocked:测试IP 1 → 访问 403
□ 日志有 WARN 记录(攻击日志)
□ CDN 场景 real_ip 生效(日志是真实用户 IP)
□ reload 后功能正常(lua_code_cache on)

九、小结

OpenResty Lua WAF 三板斧:滑动窗口动态限流(按 IP/接口分级,比 Nginx rate 精细)、请求体深度检测(SQLi/XSS/遍历,Nginx 配置够不着的地方)、Redis 全局黑名单(多机同步 + 违规计分自动封)。性能代价是每请求 0.1–0.3ms,对防护价值可以忽略。定位上它是源站最后一道自研防线——前面的 CDN/高防挡大流量,Lua WAF 挡漏过来的精细攻击,两层配合才是完整的纵深。


相关阅读:


本文由财富云计算整理,我们提供高防服务器、CDN 防护与 NAT 型云服务器,无独立公网 IP 机型价格比独立 IP 省一半以上,需要「高防 + CDN + 源站 Lua WAF」完整方案的咨询。更多教程见端口速查表。
延伸阅读
  1. 高防误区与避坑清单:25 个坑+下单前必问 14 问+到货验货流程
  2. DNS 攻击防护专题:劫持、隧道、投毒、放大四类攻击与 DNSSEC
  3. 电商大促防攻击预案:T-14 到当天的分级响应与降级开关
  4. 源站真实 IP 隐藏完全指南:六大暴露面排查+NAT 型终极方案
  5. 日志分析找攻击源:Nginx 日志三维度分析法与证据固定
  6. 攻击应急响应手册:六阶段响应流程,从确认到复盘的照做清单
  7. iptables 抗 DDoS 配置模板:状态过滤+SYN 防护+防扫描+游戏服版
  8. Nginx 进阶防护:连接层、请求层、行为层三层生产级配置
更多阅读
  1. 上一篇:Nginx 进阶防护:连接层、请求层、行为层三层生产级配置
  2. 下一篇:iptables 抗 DDoS 配置模板:状态过滤+SYN 防护+防扫描+游戏服版
发表评论 1