随着网站业务越做越大,很多朋友开始使用 OpenResty 来处理高并发请求。这玩意儿确实猛,基于 Nginx 和 Lua,性能强还灵活。但很多人没意识到,OpenResty 本身只是一把枪,子弹怎么装、保险怎么开,全靠你自己的配置和脚本。今天咱们就聊聊,怎么用 OpenResty 自己动手,把最常见的 Web 攻击挡在门外,让你晚上睡觉踏实点。
一、先说点大实话:为什么非要用 OpenResty 做安全防护
很多同学一提到安全防护,脑子里全是云盾、WAF、高防 IP。没错,这些产品确实厉害,但有几个问题:贵,而且不够灵活。你加一条自定义规则,域名解析要改,BGP 要切,等半分钟才好。而 OpenResty 就在你的服务前面,请求进来先经过它,你可以在请求到达后端业务逻辑之前,用 Lua 脚本做任意检查。
打个比方,你开了一家餐厅,后端 Java 是厨房,OpenResty 就是门口迎宾的小哥。他能在客人进门之前拦住他们,检查证件、搜包,还能根据你的心情决定让不让进。厨房不用管这些乱七八糟的事,专心炒菜就行。这样安全能力前置,你的核心业务代码保持干净,出问题也好排查。
另一个关键点:性能。OpenResty 基于 Nginx 事件模型,Lua 解释器非常轻量,跑几行正则几乎不占多少 CPU。相比之下,在业务代码里加一堆安全检查,动辄要连 Redis、写日志,反而拖慢整体响应。把安全规则下放到 OpenResty 这一层,等于给请求上了高速公路收费站的 ETC 通道,又快又不堵。
当然,OpenResty 也有缺点。第一,它不擅长做复杂的上下文关联分析,比如检测一个用户两天内的行为轨迹,你得自己写 Lua 逻辑或者调外部存储。第二,配置写错容易导致服务不可用,因为它是全局生效的。第三,社区资料不够系统,很多坑要自己踩。但这并不妨碍我们用它来对付绝大多数常见攻击,因为那些攻击本质上就那几招,我们见招拆招就够了。
二、应用场景:哪些攻击是 OpenResty 能管得住的
我们用 OpenResty 防攻击,主要是把它部署在所有 Web 请求的入口,比如反向代理层。只要请求经过它,它就可以干这几件事:
IP 黑名单和白名单:直接拒绝来自某些坏 IP 的请求,或者只允许公司内部网段访问后台。
限流防刷:同一个 IP 一秒访问超过 N 次,直接返回 503,这能挡住大部分 CC 攻击和爬虫。
UA 过滤:某些带明显攻击特征的 User-Agent,比如扫描器、僵尸网络,直接屏蔽。
SQL 注入和 XSS 拦截:用正则检查 URL 参数、POST 表单、请求头,发现危险关键词就阻断。
异常请求方法:比如 DELETE、TRACE 这些东西,不是业务需要的就关掉。
文件上传校验:限制上传文件类型、大小,防止传木马。
这些场景覆盖了 OWASP Top 10 里的大部分风险。更重要的是,我们不需要修改后端代码,只需要在 OpenResty 上改配置和重载脚本,就能做到全局防护。对于老旧系统、第三方组件黑盒部署的情况来说,这简直是救星。
三、技术优缺点:清醒认识这个方案
咱们不吹不黑,列一下 OpenResty 安全方案的优点和缺点。
先说优点。
第一个是热更新。改 Lua 脚本不需要重启 Nginx,用 nginx -s reload 或者直接通过 API 加载新的配置,秒级生效,对业务几乎无感。
第二个是内聚。所有安全逻辑集中在一个入口,避免每个业务模块各自写一套过滤函数。维护成本低,规则变更只动一个地方。
第三个是性能好。大部分检查是纯内存操作,加上正则缓存,单机可以扛住数万 QPS,比用 Node.js 或者 PHP 做同样的过滤要快得多。
第四个是可编程能力极强。OpenResty 能让你写复杂的逻辑,比如动态白名单、按条件限流、配合 Redis 做分布式计数,这比单纯用 Nginx 的 if 语句强太多了。
再说缺点。
第一,规则写不好会误伤正常用户。比如 SQL 注入检测太激进,把含有“select”的普通文章标题给拦了,那就很尴尬。
第二,Lua 代码质量参差不齐。很多人没学过 Lua,临时看两眼文档就开写,容易出内存泄露或者正则灾难性回溯问题。
第三,它只能做到基于单次请求的检测。如果攻击者把恶意代码做多重编码,或者分散到多个请求里,OpenResty 靠正则往往是防不住的。这种情况还得上语义分析组件。
第四,没有现成的大平台支持。比如云 WAF 有机器学习和专家团队持续更新规则,而你自己的 OpenResty 脚本,只能靠你自己维护。换句话说,它像一个定制武器,上限很高,但下限也很低。
四、正式开工:一个完整的 OpenResty 安全防护配置实例
从这一章开始,咱们直接上干货。我用的技术栈是 OpenResty + Lua(OpenResty 基于 Nginx,Lua 脚本负责逻辑),示例环境是 OpenResty 1.19 以上版本。所有代码放在一个示例工程里,你按顺序复制就能跑。我们先看整体目录结构:
/opt/nginx/
├── conf/
│ ├── nginx.conf # 主配置,引入安全相关的模块
│ └── lua/
│ ├── common.lua # 公共函数:日志、响应、取参数
│ ├── ip_filter.lua # IP 黑名单/白名单
│ ├── user_agent.lua # UA 过滤
│ ├── sqli_xss.lua # SQL 注入和 XSS 检测
│ ├── ratelimit.lua # 限流
│ └── block_rules.lua # 规则配置(关键词清单)
└── logs/
4.1 首先确认你的 OpenResty 装好了
如果你还没装,用下面的命令快速装一下(以 CentOS 为例):
# 安装 OpenResty 官方仓库
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
# 安装 openresty,包含 nginx 和 LuaJIT
sudo yum install -y openresty
# 检查版本
openresty -v
我假设你已经把 OpenResty 当成自己的反向代理在用,至少知道怎么启动和 reload。如果你还没配置过任何代理,先看下面这段最简配置,确保服务能起来。
4.2 配置主 nginx.conf:挂上安全钩子
我们要在 server 块里加上 access_by_lua_file,这个指令会在每个请求进入时执行我们的 Lua 安全检查。下面是一个完整的 nginx.conf 片段,请把 xxx 替换成你的实际路径:
# 文件:conf/nginx.conf (OpenResty + Lua)
worker_processes auto;
error_log logs/error.log warn;
events {
worker_connections 1024;
}
http {
lua_package_path "/opt/nginx/conf/lua/?.lua;;";
server {
listen 80;
server_name my-site.com;
# 把真实客户端 IP 传给 Lua,不然容易拿到 CDN 的 IP
set_real_ip_from 0.0.0.0/0;
real_ip_header X-Forwarded-For;
# 每个请求进来先跑这一段安全检查
access_by_lua_file /opt/nginx/conf/lua/block_rules.lua;
# 反向代理到后端服务(比如你的 Java 应用)
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
注意,我们只写了主流程,真正的安全检查全在 block_rules.lua 里,由它再调用其他模块。这样主配置干净,逻辑集中在 Lua 文件里。
4.3 写一个公共工具类:把常用功能攒起来
为了不重复造轮子,我们先写一个 common.lua,里面包含取用户 IP、写日志、返回错误页等函数。
-- 文件:common.lua
-- 技术栈:OpenResty + Lua
local _M = {}
-- 获取客户端真实 IP
-- 在 access 阶段 nginx 会填好 remote_addr
function _M.get_client_ip()
-- 如果前面有 CDN,我们通过 nginx 的 real_ip 模块已经处理过了
-- 所以直接用 ngx.var.remote_addr 就行
return ngx.var.remote_addr or ""
end
-- 记录安全日志到文件
function _M.log_security(rule_name, message)
local ip = _M.get_client_ip()
local uri = ngx.var.uri or "-"
local args = ngx.var.request_uri or "-"
-- 将时间、IP、规则名、请求信息拼起来
local log_line = string.format(
"[%s] ip=%s rule=%s uri=%s args=%s message=%s",
os.date("%Y-%m-%d %H:%M:%S"), ip, rule_name, uri, args, message
)
-- 用 ngx.log 输出到 error.log,级别为 warn
ngx.log(ngx.WARN, log_line)
end
-- 直接返回一个简单的 HTML 拒绝页面
function _M.block(rule_name, message)
_M.log_security(rule_name, message)
ngx.status = ngx.HTTP_FORBIDDEN
ngx.header.content_type = "text/html; charset=utf-8"
-- 这里返回一个简单的提示,你也可以根据业务定制
ngx.say("<h1>访问被拒绝</h1><p>你的请求触发安全规则,如有误判请联系管理员。</p>")
-- 结束请求,不再代理到后端
ngx.exit(ngx.HTTP_FORBIDDEN)
end
-- 判断一个字符串是否包含某个模式
-- 返回 true 表示包含
function _M.matches_any(input, patterns)
if input == nil or input == "" then
return false
end
for _, pattern in ipairs(patterns) do
-- 使用 ngx.re.find 会走 OpenResty 的 PCRE 库,性能更好
local from, _, err = ngx.re.find(input, pattern, "i")
if err then
ngx.log(ngx.ERR, "正则错误: " .. err)
return false
end
if from then
return true
end
end
return false
end
return _M
上面的代码请注意,ngx.re.find 支持正则缓存,比 Lua 自带的 string.find 快很多。我们要拦截的攻击特征一般都得用正则来匹配,所以用这个函数是划算的。
4.4 配置 IP 黑名单和白名单
在我们自己的系统里,IP 名单是安全的第一道门。比如公司内部后台只允许 10.0.0.0/8 网段访问,其它人一律挡掉。同时,有些恶意 IP 我们可以写死进去。下面这个 ip_filter.lua 实现了简单的 CIDR 匹配。
-- 文件:ip_filter.lua
-- 技术栈:OpenResty + Lua
local common = require "common"
-- 黑名单 IP 列表(支持精确IP)
local blacklist_ips = {
"1.2.3.4",
"5.6.7.8",
}
-- 白名单网段列表(CIDR格式)
local whitelist_cidrs = {
"10.0.0.0/8",
"192.168.1.100",
}
-- 将一个 IP 字符串转成 32 位整数
local function ip_to_num(ip_str)
local a, b, c, d = ip_str:match("(%d+)%.(%d+)%.(%d+)%.(%d+)")
if not a then
return nil
end
return tonumber(a) * 16777216 + tonumber(b) * 65536 + tonumber(c) * 256 + tonumber(d)
end
-- 判断 IP 是否在指定的 CIDR 网段内
local function ip_in_cidr(ip_str, cidr)
local ip_net, mask_bits = cidr:match("([^/]+)/(%d+)")
if not mask_bits then
-- 没有掩码,视为精确匹配
return ip_str == cidr
end
mask_bits = tonumber(mask_bits)
local ip_num = ip_to_num(ip_str)
local net_num = ip_to_num(ip_net)
if not ip_num or not net_num then
return false
end
local shift = 32 - mask_bits
-- 把网络地址和IP地址都移位,然后比较高位部分
return (ip_num >> shift) == (net_num >> shift)
end
-- 执行检查,如果被拦截则调用 common.block
function _M.check()
local ip = common.get_client_ip()
-- 先查黑名单
for _, black_ip in ipairs(blacklist_ips) do
if ip == black_ip then
common.block("ip_blacklist", "匹配黑名单IP: " .. black_ip)
return
end
end
-- 如果命中了白名单网段,直接放行(注意顺序:白名单优先)
for _, cidr in ipairs(whitelist_cidrs) do
if ip_in_cidr(ip, cidr) then
return
end
end
-- 这里故意不做“默认拒绝”,只做“默认放行”
-- 但如果你想要更严格的模式,可以在这里 block()
end
return _M
细心的朋友会发现,我并没有把 blacklist_ips 和 whitelist_cidrs 放到单独配置文件里,直接写在 Lua 里方便演示。真实业务中,建议把规则放到 Redis 或者独立的 rules.lua 文件中,避免每次改 IP 都要改脚本。
4.5 过滤恶意 User-Agent
很多扫描器和爬虫会用特定的 UA 字符串,比如 sqlmap、nmap、python-requests 之类的。有一些是正经的,比如 python-requests 是程序员用的;但为了让演示更明确,我们这里拦截 “采集型” UA 和常见的恶意工具 UA。你按照自己业务调整即可。
-- 文件:user_agent.lua
-- 技术栈:OpenResty + Lua
local common = require "common"
-- 恶意或可疑的 User-Agent 特征
local bad_uas = {
"sqlmap",
"nmap",
"nikto",
"nessus",
"masscan",
"python-requests",
"scrapy",
"purebot",
}
-- 检查 UA
function _M.check()
-- 取请求头中的 user-agent
local ua = ngx.var.http_user_agent or ""
-- 这里注意:如果 UA 为空,某些爬虫可能故意去掉 UA,我们根据业务需要决定是否拦截
if ua == "" then
-- 很多正常手机浏览器也会不带 UA,但是很少见
-- 为了演示,我们不对空 UA 做处理
return
end
for _, bad_ua in ipairs(bad_uas) do
-- 大小写不敏感匹配
if string.find(ua:lower(), bad_ua) then
common.block("bad_user_agent", "匹配UA: " .. bad_ua)
return
end
end
end
return _M
代码很简单,就是取 UA 头,然后逐个匹配关键词。注意这里用了 string.find 而不是正则,因为匹配的只是固定子串,没必要用正则。
4.6 写一个 SQL 注入和 XSS 的过滤器
这是安全防护里的重头戏。我们不需要特别复杂的语义分析,只需要在请求参数里匹配一组危险关键词。为了性能,我们把常见 SQL 注入和 XSS 特征放在一个数组里,统一匹配。但注意,真实防御不能只靠这个,后面我会在“注意事项”里说清楚。
-- 文件:sqli_xss.lua
-- 技术栈:OpenResty + Lua
local common = require "common"
-- 危险特征列表,这里仅做典型演示,实际使用时请根据业务调优
local dangerous_patterns = {
-- SQL 注入特征
"['\"]%s*or%s*['\"]", -- 匹配 'or' 或 "or"
"union%s+select", -- union select
"select.+from",
"insert%s+into",
"delete%s+from",
"update%s+set",
"drop%s+table",
"--", -- SQL 注释
"#", -- MySQL 注释(注意,这个容易误伤)
"\\bexec\\b",
-- XSS 特征
"<script",
"javascript:",
"onerror%s*=",
"onmouseover%s*=",
"onclick%s*=",
}
-- 递归遍历 table,把所有参数值拼成一个字符串
local function collect_values_from_args(args)
local result = ""
if type(args) ~= "table" then
return tostring(args or "")
end
for _, value in pairs(args) do
if type(value) == "table" then
-- 处理多值参数,比如 ?id=1&id=2
for _, v in ipairs(value) do
result = result .. " " .. tostring(v)
end
else
result = result .. " " .. tostring(value)
end
end
return result
end
-- 检查主逻辑,在 access 阶段执行
function _M.check()
-- 获取所有请求参数(包括 GET 和 POST)
-- ngx.req.get_uri_args 拿到的是原始参数,ngx.req.get_post_args 需要单独读取 body
local uri_args = ngx.req.get_uri_args()
-- 合并 GET 和 POST 参数
local body_args = {}
-- 如果请求有 body,才去读
if ngx.req.get_method() == "POST" then
-- 注意:这里需要先读取 body,并且在 access 阶段调用 ngx.req.read_body()
ngx.req.read_body()
body_args = ngx.req.get_post_args()
end
-- 把所有参数值合并成一个字符串用于正则匹配
local all_params = {}
table.insert(all_params, collect_values_from_args(uri_args))
table.insert(all_params, collect_values_from_args(body_args))
local combined = table.concat(all_params, " ")
-- 检测是否包含危险特征
if common.matches_any(combined, dangerous_patterns) then
common.block("sqli_xss", "请求参数包含攻击特征")
end
end
return _M
这里有个细节,ngx.req.get_post_args() 必须在调用 ngx.req.read_body() 之后才能获取到 POST 内容。如果请求体很大,access 阶段直接读 body 可能会影响性能,所以 OpenResty 里一般建议在 access 阶段只做简单检查,或者干脆把检查放到 rewrite 阶段。为了演示,我们就在 access 阶段做了,真实产品可以按需调整。
4.7 实现限流:防止 CC 攻击
CC 攻击说白了就是大量请求来耗死你的服务。我们用 OpenResty 自带的 ngx.shared.DICT 做计数器,统计每个 IP 在一秒内访问次数,超过阈值就拒绝。下面是一个简单可靠的滑动窗口(用固定时间窗口也可以,演示就用固定窗口,比较直观)。
-- 文件:ratelimit.lua
-- 技术栈:OpenResty + Lua
local common = require "common"
-- 建立一个共享内存字典,需要在 nginx.conf 里配置 lua_shared_dict 才能用
-- 配置文件里写上:
-- lua_shared_dict limit_dict 10m;
-- 限流参数
local RATE_LIMIT_SECONDS = 1 -- 统计的时间窗口(秒)
local MAX_REQUESTS = 5 -- 每个 IP 在窗口内允许的最大请求次数
-- 检查函数
function _M.check()
local ip = common.get_client_ip()
-- 从共享字典里获取当前计数
-- 我们使用“当前时间戳除以窗口大小”作为键的一部分,这样自动分桶
local key = string.format("%s:%d", ip, math.floor(ngx.now() / RATE_LIMIT_SECONDS))
-- 获取共享字典对象
local dict = ngx.shared.limit_dict
if not dict then
-- 如果没有配置共享内存,就直接放行,避免服务不可用
return
end
-- incr 方法,如果 key 不存在则初始化为 0
local new_count, err = dict:incr(key, 1)
if not new_count then
-- 第一次访问,key 不存在,用 add 初始化
local ok, err = dict:add(key, 1, RATE_LIMIT_SECONDS + 1)
if not ok then
-- 可能有并发冲突,无伤大雅,直接放行
return
end
new_count = 1
end
-- 如果超过限制,就返回 503
if new_count > MAX_REQUESTS then
common.block("limit_rate", "超过请求频率限制,IP: " .. ip)
end
end
return _M
注意,限流不应该直接返回 403,通常返回 503 或者自定义一个“请求过于频繁”的提示。这里我直接用 common.block 返回 403 其实不太合适,更好的做法是返回 503。你可以单独写一个 ratelimit_block 函数,或者给 block 函数增加一个状态码参数,我这里为了代码复用就简单处理了。
4.8 把这些模块组装到总入口:block_rules.lua
现在我们要把以上几个模块串起来。注意执行顺序:先查 IP 黑白名单,再查 UA,再查 SQL 注入/XSS,最后限流。因为限流需要优先知道是否“正常请求”,如果前面已经拦截,就不计入限流次数?但真实场景中,攻击请求也会占用次数。这个看你的策略。我在这里按“先安全后性能”的顺序:先做黑白名单,再做参数过滤,最后限流。
-- 文件:block_rules.lua
-- 技术栈:OpenResty + Lua
-- 这是 access_by_lua_file 指定的入口模块
local common = require "common"
local ip_filter = require "ip_filter"
local user_agent = require "user_agent"
local sqli_xss = require "sqli_xss"
local ratelimit = require "ratelimit"
-- 第一步:IP 检查
ip_filter.check()
-- 第二步:UA 检查
user_agent.check()
-- 第三步:SQL 注入和 XSS 检查
sqli_xss.check()
-- 第四步:限流(放在最后,因为限流会读取参数,但不影响安全判断)
ratelimit.check()
等等,common.block 内部调用了 ngx.exit,所以一旦触发拦截,这个请求就在访问阶段直接终止了,后续不会继续执行。那 ratelimit.check() 还会执行吗?不会,因为 ngx.exit 会跳出当前请求的整个处理流程。所以实际上顺序完全没问题。但要注意,如果 ip_filter.check() 判断命中白名单后只是 return,并不会中断整个 block_rules.lua 的执行。也就是说,我们的白名单只是跳过 IP 检查,并不跳过后面的 UA、SQL 注入和限流。这其实是合理的,因为白名单只是关于 IP 访问权限,不代表允许恶意请求。
4.9 配置共享内存并启用真实 IP
在 nginx.conf 的 http 块里需要加上 lua_shared_dict,否则限流脚本会直接放行。同时,如果你用了 CDN,还需要正确设置 set_real_ip_from。下面是一段完整的启用了安全模块的 nginx.conf,注意我们还要把 access_by_lua_file 指向 block_rules.lua。
# 文件:conf/nginx.conf (OpenResty + Lua)
worker_processes 2;
error_log logs/error.log warn;
events {
worker_connections 1024;
}
http {
# 加载 Lua 模块路径
lua_package_path "/opt/nginx/conf/lua/?.lua;;";
# 共享内存,用于限流计数
lua_shared_dict limit_dict 10m;
# 如果你的服务前面有 CDN 或负载均衡,需要设置信任代理,以便拿到真实 IP
set_real_ip_from 0.0.0.0/0;
real_ip_header X-Forwarded-For;
server {
listen 80;
server_name my-site.com;
# 每个请求进入先执行安全检查
access_by_lua_file /opt/nginx/conf/lua/block_rules.lua;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
到这里,一个基于 OpenResty + Lua 的基础安全防护体系就搭起来了。你可以在本地跑一个后端服务,然后用 curl 尝试模拟攻击请求,看是否被拦截。
4.10 用 curl 实测一下效果
我们来模拟几个请求,看看拦截是否生效。首先启动你的 OpenResty(假设你已经在 /opt/nginx 目录下),然后编译并启动:
# 进入 OpenResty 目录
cd /opt/nginx
# 检查配置是否正确
openresty -t -p /opt/nginx
# 启动或重载
openresty -p /opt/nginx -c conf/nginx.conf
# 重新加载配置
openresty -s reload -p /opt/nginx
然后测试:
# 正常请求,应该能访问后端(返回 200 或后端内容)
curl -I http://127.0.0.1/?id=100
# 模拟 SQL 注入
curl "http://127.0.0.1/?id=1' or '1'='1"
# 模拟 XSS
curl "http://127.0.0.1/?name=<script>alert(1)</script>"
# 模拟恶意 UA
curl -H "User-Agent: sqlmap/1.0" http://127.0.0.1/
# 快速刷新多次触发限流
for i in {1..10}; do curl http://127.0.0.1/?a=1; done
注意,如果你的后端服务没有启动,正常请求可能会收到 502,但安全拦截的请求会直接收到 403 或者 503。你可以在 error.log 里看到我们写的 warn 日志。
五、注意事项:别让这份方案变成伤你的刀
5.1 正则别写太野
有些同学为了拦截得全面,会写非常复杂的正则。比如 .*(union.*select).*,这种在不加 o 选项时性能很差。而且 OpenResty 的 PCRE 也有超时保护,但你要主动设置 pcre_jit 和 jit 机制。我建议你每次写正则后做个压测,看看 QPS 下降多少。
5.2 小心误伤正常业务
你要仔细检查自己的业务参数到底允许哪些特殊字符。比如文章正文里本来就包含 <script> 作为示例代码,你直接拦截会把正常用户挡掉。解决方式是按参数名区分:某些参数走严格检测,某些参数跳过。或者把检测放到 body_filter 阶段,但那又太晚。总之规则越精细越好,不要一刀切。
5.3 读取 POST 对内存的影响
在 access 阶段调用 ngx.req.read_body() 会把整个请求体读到内存中。如果你的接口允许上传大文件,如 100MB,那这个操作可能直接撑爆内存。所以对于文件上传接口,要么在 location /upload 这一层不启用读取 body 的检查,要么单独用 client_body_buffer_size 控制。我建议你把安全规则允许配置为按 location 差异化开关。
5.4 不要依赖纯正则防御全部攻击
SQL 注入和 XSS 的变种太多,编码、二次拼接都可能绕过正则。如 %27 这种 URL 编码,OpenResty 在 ngx.req.get_uri_args 返回的是解码后的参数,所以你的正则需要对解码后的值做检测,这样就安全一些。但更复杂的,比如 and 1=2 这种没有明显特征的操作,就很难靠特征拦截。因此,OpenResty 防护更像是一道围墙,真正确保安全还是要后端参数化查询和输出转义。
5.5 日志很重要
安全模块一定要记录足够详细的日志,不然攻击发生了你还不知道是谁干的,或者你误伤了用户,连投诉都查不到。建议将日志输出到独立的文件中,并配合日志切割和分析工具。比如在 common.log_security 里写到 ngx.WARN,你可以通过 logrotate 把 error.log 按天轮转。
六、总结:有了它,你至少能挡掉 80% 的乱拳
OpenResty 安全方案不是万能的,但对于绝大多数的“脚本小子”和初级攻击者来说,已经足够让他们打退堂鼓。它成本低、性能好、规则灵活,而且天然位于所有请求的前沿,你的后端业务代码不需要做任何改动。你可以把 IP 黑白名单、UA 过滤、SQL 注入/XSS 基础拦截、限流等能力集中在一个小脚本里,随时热更新,轻松应对突发的扫描和暴力请求。
但我也想给你提个醒:安全是一个持续对抗的过程。今天你拦住了 union select,明天攻击者可能用注释符、内联注释,甚至 JSON 编码来绕过。所以这个方案应该作为你整体安全体系的第一层防护,后续还需要配合 WAF、入侵检测、日志分析、人工渗透测试等工具,才能做到相对安心。
最后,如果你身边也有同事正在被这些问题困扰,不妨把这篇文章发给他。一起把 OpenResty 用得更扎实,让应用稳如老狗。
评论
围绕“OpenResty安全加固方案:防范常见Web攻击的配置与脚本实现”参与讨论