第 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("&", "&")
str = str:gsub("<", "<")
str = str:gsub(">", ">")
str = str:gsub('"', """)
str = str:gsub("'", "'")
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 章 - 微服务网关架构 →