强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 12 章 - 安全防护

第 12 章 - 安全防护

12.1 安全威胁全景

                 攻击类型
                    │
    ┌───────────────┼───────────────┐
    │               │               │
应用层攻击      协议层攻击      资源耗尽攻击
    │               │               │
├─ SQL 注入     ├─ HTTP 洪水    ├─ CC 攻击
├─ XSS          ├─ Slowloris    ├─ 连接耗尽
├─ CSRF         └─ 畸形请求     └─ 内存耗尽
├─ 路径遍历
├─ 命令注入
└─ 文件上传

12.2 IP 黑白名单

12.2.1 静态 IP 黑名单

# 使用 Nginx 原生模块
geo $blocked_ip {
    default         0;
    192.168.100.0/24 1;   # 整个子网
    10.0.0.1         1;   # 单个 IP
    include /etc/nginx/blacklist.conf;  # 外部文件
}

server {
    listen 8080;

    if ($blocked_ip) {
        return 403;
    }
}

12.2.2 Lua 动态 IP 黑名单

-- /usr/local/openresty/lua/security/ip_blacklist.lua
local _M = {}

local cjson = require "cjson"

-- 从共享内存加载黑名单
local function is_blocked(ip)
    local blacklist = ngx.shared.blacklist
    if not blacklist then return false end

    -- 精确匹配
    if blacklist:get(ip) then
        return true, "exact"
    end

    -- CIDR 匹配(简化版,生产建议使用 lua-resty-iputils)
    local iputils = require "resty.iputils"
    local cidrs = blacklist:get_keys(0)
    for _, cidr in ipairs(cidrs) do
        if cidr:match("/") then
            local ok = iputils.ip_in_cidr(ip, cidr)
            if ok then
                return true, "cidr"
            end
        end
    end

    return false
end

-- 动态封禁(基于行为检测)
function _M.check_and_block()
    local ip = ngx.var.remote_addr

    -- 检查黑名单
    local blocked, reason = is_blocked(ip)
    if blocked then
        ngx.log(ngx.WARN, "Blocked IP: ", ip, " reason: ", reason)
        ngx.status = 403
        ngx.header["Content-Type"] = "application/json"
        ngx.say(cjson.encode({
            error = "Forbidden",
            message = "Your IP has been blocked",
        }))
        return ngx.exit(403)
    end

    -- 记录请求频率(用于后续检测)
    local stats = ngx.shared.request_stats
    local count = stats:incr(ip, 1, 0, 60)

    -- 自动封禁:1 分钟内超过 1000 次请求
    if count > 1000 then
        local blacklist = ngx.shared.blacklist
        blacklist:set(ip, "auto_blocked", 3600)  -- 封禁 1 小时
        ngx.log(ngx.WARN, "Auto-blocked IP: ", ip, " requests: ", count)
    end
end

-- 管理接口:手动封禁/解封
function _M.admin_api()
    local method = ngx.req.get_method()
    ngx.req.read_body()
    local body = ngx.req.get_body_data()
    local data = body and cjson.decode(body) or {}

    local blacklist = ngx.shared.blacklist

    if method == "POST" then
        -- 封禁 IP
        local ttl = data.ttl or 3600
        blacklist:set(data.ip, data.reason or "manual", ttl)
        ngx.say(cjson.encode({status = "blocked", ip = data.ip}))

    elseif method == "DELETE" then
        -- 解封 IP
        blacklist:delete(data.ip)
        ngx.say(cjson.encode({status = "unblocked", ip = data.ip}))

    elseif method == "GET" then
        -- 查看黑名单
        local keys = blacklist:get_keys(0)
        local list = {}
        for _, key in ipairs(keys) do
            table.insert(list, {ip = key, reason = blacklist:get(key)})
        end
        ngx.say(cjson.encode({blocked_ips = list}))
    end
end

return _M

12.2.3 白名单模式(内网/API Key 用户)

-- 白名单检查
local function is_whitelisted(ip)
    local whitelist = {
        "10.0.0.0/8",
        "172.16.0.0/12",
        "192.168.0.0/16",
        "127.0.0.1",
    }

    local iputils = require "resty.iputils"
    for _, cidr in ipairs(whitelist) do
        if iputils.ip_in_cidr(ip, cidr) then
            return true
        end
    end
    return false
end

12.3 SQL 注入防护

12.3.1 SQL 注入检测

-- /usr/local/openresty/lua/security/sql_injection.lua
local _M = {}

-- SQL 注入特征模式
local sqli_patterns = {
    -- 基本注入
    "'%s*OR%s+%d+%s*=%s*%d+",
    "'%s*OR%s+'%w+'%s*=%s*'%w+'",
    "'%s*AND%s+%d+%s*=%s*%d+",
    "UNION%s+SELECT",
    "UNION%s+ALL%s+SELECT",

    -- 注释绕过
    "/%*.+%*/",
    "--%s",
    "#",

    -- 常见函数
    "SLEEP%s*%(",
    "BENCHMARK%s*%(",
    "WAITFOR%s+DELAY",
    "LOAD_FILE%s*%(",
    "INTO%s+OUTFILE",
    "INTO%s+DUMPFILE",

    -- 信息收集
    "INFORMATION_SCHEMA",
    "CONCAT%s*%(",
    "GROUP_CONCAT%s*%(",
    "CHAR%s*%(",
    "0x[0-9a-fA-F]+",

    -- 时间盲注
    "IF%s*%([^,]+,[^,]+,SLEEP",
    "CASE%s+WHEN",
}

-- 编译正则(性能优化)
local compiled_patterns = {}
for _, pattern in ipairs(sqli_patterns) do
    table.insert(compiled_patterns, {
        regex = pattern,
        compiled = ngx.re.compile(pattern, "joi"),
    })
end

-- 检测 SQL 注入
function _M.detect(input)
    if not input or input == "" then
        return false
    end

    -- URL 解码
    input = ngx.unescape_uri(input)

    for _, p in ipairs(compiled_patterns) do
        local match, err = ngx.re.find(input, p.regex, "joi")
        if match then
            return true, p.regex
        end
    end

    return false
end

-- 检查所有输入参数
function _M.check_all_inputs()
    -- 检查 URI 参数
    local args = ngx.req.get_uri_args()
    for k, v in pairs(args) do
        if type(v) == "string" then
            local found, pattern = _M.detect(v)
            if found then
                return true, "query_param:" .. k, pattern
            end
        end
    end

    -- 检查 POST body
    if ngx.req.get_method() == "POST" then
        ngx.req.read_body()
        local body = ngx.req.get_body_data()
        if body then
            local found, pattern = _M.detect(body)
            if found then
                return true, "body", pattern
            end
        end
    end

    -- 检查 Headers
    local headers = ngx.req.get_headers()
    for k, v in pairs(headers) do
        if type(v) == "string" then
            local found, pattern = _M.detect(v)
            if found then
                return true, "header:" .. k, pattern
            end
        end
    end

    return false
end

-- 防护中间件
function _M.protect()
    local found, location, pattern = _M.check_all_inputs()

    if found then
        ngx.log(ngx.WARN, "SQL Injection detected from ",
            ngx.var.remote_addr, " location: ", location, " pattern: ", pattern)

        -- 记录攻击日志
        ngx.timer.at(0, function()
            local cjson = require "cjson"
            local fd = io.open("/var/log/openresty/waf.log", "a")
            if fd then
                fd:write(cjson.encode({
                    type = "sql_injection",
                    ip = ngx.var.remote_addr,
                    uri = ngx.var.uri,
                    location = location,
                    pattern = pattern,
                    timestamp = ngx.now(),
                }) .. "\n")
                fd:close()
            end
        end)

        ngx.status = 403
        ngx.header["Content-Type"] = "application/json"
        ngx.say('{"error":"Forbidden","message":"Request blocked by WAF"}')
        return ngx.exit(403)
    end
end

return _M

12.4 XSS 防护

-- /usr/local/openresty/lua/security/xss_protection.lua
local _M = {}

-- XSS 检测模式
local xss_patterns = {
    "<script[^>]*>",
    "</script>",
    "javascript:",
    "on\\w+\\s*=",           -- 事件处理器 onclick=, onerror= 等
    "expression\\s*%(",      -- CSS expression
    "vbscript:",
    "data:text/html",
    "<iframe[^>]*>",
    "<object[^>]*>",
    "<embed[^>]*>",
    "<form[^>]*>",
    "document\\.(cookie|domain|write)",
    "window\\.(location|open)",
    "eval\\s*%(",
    "alert\\s*%(",
}

-- HTML 实体编码
function _M.escape_html(str)
    if not str then return "" end
    str = str:gsub("&", "&amp;")
    str = str:gsub("<", "&lt;")
    str = str:gsub(">", "&gt;")
    str = str:gsub('"', "&quot;")
    str = str:gsub("'", "&#39;")
    return str
end

-- 检测 XSS 攻击
function _M.detect(input)
    if not input then return false end

    for _, pattern in ipairs(xss_patterns) do
        local match = ngx.re.find(input, pattern, "joi")
        if match then
            return true, pattern
        end
    end
    return false
end

-- XSS 防护中间件
function _M.protect()
    local args = ngx.req.get_uri_args()
    for k, v in pairs(args) do
        if type(v) == "string" then
            local found = _M.detect(v)
            if found then
                ngx.status = 403
                ngx.say('{"error":"XSS attempt detected"}')
                return ngx.exit(403)
            end
        end
    end
end

-- 响应头安全设置
function _M.set_security_headers()
    ngx.header["X-XSS-Protection"] = "1; mode=block"
    ngx.header["X-Content-Type-Options"] = "nosniff"
    ngx.header["Content-Security-Policy"] = "default-src 'self'"
    ngx.header["X-Frame-Options"] = "SAMEORIGIN"
    ngx.header["Referrer-Policy"] = "strict-origin-when-cross-origin"
end

return _M

12.5 CC 攻击防护

CC(Challenge Collapsar)攻击模拟正常用户发送大量请求消耗服务器资源。

-- /usr/local/openresty/lua/security/cc_protection.lua
local _M = {}

local cjson = require "cjson"

-- CC 防护配置
local config = {
    -- 请求频率限制
    rate_limit = {
        window = 10,          -- 10 秒窗口
        max_requests = 100,   -- 最大 100 次请求
    },
    -- URI 访问频率限制
    uri_limit = {
        window = 60,
        max_requests = 30,    -- 同一 URI 每分钟最多 30 次
    },
    -- 验证码触发阈值
    captcha_threshold = 80,   -- 达到限流 80% 时触发验证码
    -- 自动封禁
    auto_ban = {
        enabled = true,
        threshold = 3,        -- 触发 3 次限流后封禁
        duration = 3600,      -- 封禁 1 小时
    },
}

-- 请求特征指纹
local function get_fingerprint()
    local parts = {
        ngx.var.remote_addr,
        ngx.var.http_user_agent or "",
        ngx.var.http_accept_language or "",
    }
    return ngx.md5(table.concat(parts, "|"))
end

-- CC 防护检查
function _M.check()
    local ip = ngx.var.remote_addr
    local uri = ngx.var.uri
    local fp = get_fingerprint()
    local stats = ngx.shared.cc_stats

    -- 1. 全局 IP 频率检查
    local ip_key = "cc:ip:" .. ip
    local ip_count = stats:incr(ip_key, 1, 0, config.rate_limit.window)

    if ip_count > config.rate_limit.max_requests then
        -- 触发限流
        _M.handle_rate_limit(ip, fp, "ip_rate")
        return false
    end

    -- 2. URI 访问频率检查
    local uri_key = "cc:uri:" .. fp .. ":" .. uri
    local uri_count = stats:incr(uri_key, 1, 0, config.uri_limit.window)

    if uri_count > config.uri_limit.max_requests then
        _M.handle_rate_limit(ip, fp, "uri_rate")
        return false
    end

    -- 3. 异常行为检测
    if _M.detect_abnormal_behavior() then
        _M.handle_rate_limit(ip, fp, "abnormal")
        return false
    end

    return true
end

-- 异常行为检测
function _M.detect_abnormal_behavior()
    local ua = ngx.var.http_user_agent or ""

    -- 没有 User-Agent
    if ua == "" then
        return true
    end

    -- 已知恶意 UA
    local bad_uas = {
        "python%-requests",
        "curl/",
        "wget",
        "scrapy",
        "bot",
        "spider",
    }

    -- 注意:生产环境应该有更精确的检测
    for _, pattern in ipairs(bad_uas) do
        if ua:lower():find(pattern) then
            -- 不直接封禁,但标记可疑
            ngx.var.suspicious = "1"
        end
    end

    return false
end

-- 处理限流
function _M.handle_rate_limit(ip, fp, reason)
    local stats = ngx.shared.cc_stats

    -- 记录触发次数
    local ban_key = "cc:ban_count:" .. fp
    local ban_count = stats:incr(ban_key, 1, 0, 3600)

    -- 自动封禁
    if config.auto_ban.enabled and ban_count >= config.auto_ban.threshold then
        local blacklist = ngx.shared.blacklist
        blacklist:set(ip, "cc_auto_ban:" .. reason, config.auto_ban.duration)
        ngx.log(ngx.WARN, "CC auto-banned IP: ", ip, " reason: ", reason)
    end

    ngx.status = 429
    ngx.header["Content-Type"] = "application/json"
    ngx.header["Retry-After"] = "60"
    ngx.say(cjson.encode({
        error = "Too Many Requests",
        message = "Rate limit exceeded. Please slow down.",
    }))
end

return _M

12.6 防爬虫

-- /usr/local/openresty/lua/security/bot_detection.lua
local _M = {}

-- 已知爬虫 User-Agent
local known_bots = {
    "Googlebot", "Bingbot", "Slurp", "DuckDuckBot",
    "Baiduspider", "YandexBot", "Sogou",
}

-- 可疑爬虫特征
local suspicious_patterns = {
    "headless", "phantom", "selenium", "webdriver",
    "automation", "crawler", "spider", "scraper",
}

-- 验证搜索引擎爬虫(通过反向 DNS)
local function verify_bot(ip, ua)
    local is_search_engine = false
    for _, bot in ipairs(known_bots) do
        if ua:find(bot) then
            is_search_engine = true
            break
        end
    end

    if not is_search_engine then
        return false, "not_search_engine"
    end

    -- 反向 DNS 验证
    local resolver = require "resty.dns.resolver"
    local r, err = resolver:new({nameservers = {"8.8.8.8"}})
    if not r then
        return false, "dns_error"
    end

    local answers, err = r:reverse_query(ip)
    if not answers then
        return false, "reverse_dns_failed"
    end

    -- 检查域名是否匹配搜索引擎
    for _, ans in ipairs(answers) do
        if ans.ptrdname then
            for _, bot in ipairs(known_bots) do
                if ans.ptrdname:lower():find(bot:lower()) then
                    return true, "verified"
                end
            end
        end
    end

    return false, "dns_mismatch"
end

-- 行为分析
local function analyze_behavior()
    local stats = ngx.shared.bot_stats
    local ip = ngx.var.remote_addr
    local uri = ngx.var.uri

    -- 请求频率
    local req_key = "bot:req:" .. ip
    local req_count = stats:incr(req_key, 1, 0, 60)

    -- URI 多样性
    local uri_key = "bot:uri:" .. ip
    local uri_set = stats:get(uri_key) or {}
    if type(uri_set) == "string" then
        uri_set = cjson.decode(uri_set)
    end
    uri_set[uri] = true

    local unique_uris = 0
    for _ in pairs(uri_set) do unique_uris = unique_uris + 1 end
    stats:set(uri_key, cjson.encode(uri_set), 60)

    -- 爬虫评分
    local score = 0
    if req_count > 100 then score = score + 30 end
    if unique_uris > 50 then score = score + 40 end

    return score
end

-- 爬虫防护中间件
function _M.protect()
    local ua = ngx.var.http_user_agent or ""
    local ip = ngx.var.remote_addr

    -- 检查 UA
    if ua == "" then
        ngx.status = 403
        ngx.say('{"error":"User-Agent required"}')
        return ngx.exit(403)
    end

    -- 检查可疑特征
    for _, pattern in ipairs(suspicious_patterns) do
        if ua:lower():find(pattern) then
            ngx.log(ngx.WARN, "Suspicious bot detected: ", ua)
            ngx.status = 403
            ngx.say('{"error":"Access denied"}')
            return ngx.exit(403)
        end
    end

    -- 行为分析
    local score = analyze_behavior()
    if score > 70 then
        ngx.log(ngx.WARN, "Bot behavior detected, score: ", score, " IP: ", ip)
        -- 可以选择验证码验证或直接封禁
    end
end

return _M

12.7 路径遍历防护

-- /usr/local/openresty/lua/security/path_traversal.lua
local _M = {}

-- 路径遍历检测
local traversal_patterns = {
    "%.%./",           -- ../
    "%.%..%%2f",       -- URL 编码的 ../
    "%%2e%%2e%%2f",    -- 双重 URL 编码
    "%.%..\\",         -- Windows 路径
    "/etc/passwd",
    "/etc/shadow",
    "proc/self",
    "windows/system32",
}

function _M.detect(input)
    if not input then return false end

    -- 解码
    local decoded = ngx.unescape_uri(input)
    decoded = ngx.unescape_uri(decoded)  -- 双重解码

    for _, pattern in ipairs(traversal_patterns) do
        if decoded:lower():find(pattern) then
            return true, pattern
        end
    end

    -- 检查路径中是否有 null 字节
    if decoded:find("%z") then
        return true, "null_byte"
    end

    return false
end

function _M.protect()
    local uri = ngx.var.uri
    local args = ngx.req.get_uri_args()

    -- 检查 URI
    if _M.detect(uri) then
        ngx.status = 403
        ngx.say('{"error":"Path traversal detected"}')
        return ngx.exit(403)
    end

    -- 检查参数
    for k, v in pairs(args) do
        if type(v) == "string" and _M.detect(v) then
            ngx.status = 403
            ngx.say('{"error":"Path traversal detected"}')
            return ngx.exit(403)
        end
    end
end

return _M

12.8 综合 WAF 中间件

-- /usr/local/openresty/lua/security/waf.lua
local _M = {}

local sqli = require "security.sql_injection"
local xss = require "security.xss_protection"
local path_traversal = require "security.path_traversal"
local cc = require "security.cc_protection"
local bot = require "security.bot_detection"
local ip_bl = require "security.ip_blacklist"

-- WAF 规则配置
local rules = {
    {name = "ip_blacklist",  check = ip_bl.check_and_block,    enabled = true},
    {name = "cc_protection", check = cc.check,                 enabled = true},
    {name = "bot_detection", check = bot.protect,              enabled = true},
    {name = "sql_injection", check = sqli.protect,             enabled = true},
    {name = "xss_protection", check = xss.protect,             enabled = true},
    {name = "path_traversal", check = path_traversal.protect,  enabled = true},
}

-- 白名单路径(跳过 WAF)
local whitelist_paths = {
    "/api/health",
    "/api/version",
    "/metrics",
}

-- 主检查函数
function _M.protect()
    local uri = ngx.var.uri

    -- 白名单检查
    for _, path in ipairs(whitelist_paths) do
        if uri == path then
            return true
        end
    end

    -- 执行 WAF 规则
    for _, rule in ipairs(rules) do
        if rule.enabled then
            local ok, err = pcall(rule.check)
            if not ok then
                ngx.log(ngx.ERR, "WAF rule error [", rule.name, "]: ", err)
                -- 规则出错不阻断请求
            end
            -- 如果规则返回 false 或调用了 ngx.exit,请求会被阻断
        end
    end

    return true
end

-- 安全头设置
function _M.set_headers()
    xss.set_security_headers()
    ngx.header["X-Gateway"] = "OpenResty-Gateway"
end

return _M

nginx 配置

lua_shared_dict blacklist     10m;
lua_shared_dict request_stats 50m;
lua_shared_dict cc_stats      50m;
lua_shared_dict bot_stats     20m;

server {
    listen 8080;

    # WAF 防护
    access_by_lua_block {
        local waf = require "security.waf"
        waf.protect()
    }

    # 安全响应头
    header_filter_by_lua_block {
        local waf = require "security.waf"
        waf.set_headers()
    }

    # WAF 管理接口
    location /admin/waf {
        content_by_lua_block {
            local ip_bl = require "security.ip_blacklist"
            ip_bl.admin_api()
        }
    }

    # 正常业务路由
    location /api/ {
        proxy_pass http://backend;
    }
}

12.9 注意事项

误报问题:WAF 规则可能产生误报,尤其是 SQL 注入检测。生产环境需要日志记录所有拦截事件,定期审核并调整规则。

性能影响:每增加一条 WAF 规则,都会增加请求处理延迟。建议对规则按优先级排序,高频规则放前面,低频规则放后面。

规则更新:安全威胁不断演变,WAF 规则需要定期更新。可以将规则存储在 Redis 中,支持热更新。

白名单管理:内部 API 调用、健康检查等路径应该加入白名单,避免被误拦截。


上一章← 第 11 章 - 日志与监控 下一章第 13 章 - 微服务网关架构 →