在分布式网关中,Nginx可通过OpenResty的access_by_lua_block阶段对OPTIONS预检请求动态校验Origin等元信息是否匹配租户级白名单,支持shared_dict缓存、正则匹配与多源配置热更新,并短路返回204或403,兼顾安全与性能。

在分布式网关架构中,Nginx 的 OPTIONS 请求通常用于 CORS 预检,本身不携带业务数据,但必须快速、安全地响应。若需在此阶段动态校验请求来源是否在权限白名单内(例如按域名、IP、API 路径或租户 ID 动态判断),可结合 OpenResty 的 Lua 脚本实现轻量、低延迟的决策逻辑,无需回源或调用后端服务。
明确 OPTIONS 请求的处理边界
OPTIONS 是预检请求,浏览器发起它时不会附带 Cookie、Authorization 等敏感头(除非显式配置 credentials),也不执行实际业务逻辑。因此白名单校验应聚焦于请求元信息:请求头中的 Origin、Access-Control-Request-Method、Access-Control-Request-Headers,以及客户端 IP、匹配的 upstream 路由路径等。Lua 脚本应在 access_by_lua* 阶段介入,早于 content_by_lua*,避免无效转发。
用 Lua 从多源动态获取白名单规则
白名单不应硬编码,而应支持运行时更新。常见策略包括:
- 从共享内存(
shared_dict)读取已缓存的白名单条目,配合定时器(timer.at)或 Pub/Sub(如 Redis channel)触发热更新 - 首次未命中时,通过
resty.redis或resty.http同步查询中心化配置服务(如 Nacos、Apollo),并写入shared_dict缓存,设置 TTL 防止雪崩 - 对
Origin做模式匹配(如string.match(origin, "^https?://[a-z0-9.-]+%.example%.com$")),支持通配符或正则,避免全量比对 - 结合请求路径提取租户标识(如
/t/{tenant_id}/api/xxx),再查该租户专属白名单,实现租户级隔离
在 access_by_lua_block 中完成校验与响应
直接在 Nginx 配置的 location 块中嵌入 Lua,拦截 OPTIONS 并短路响应:
location / {
# 只对 OPTIONS 方法执行校验
if ($request_method = 'OPTIONS') {
access_by_lua_block {
local core = require "resty.core"
local match = ngx.re.match
local origin = ngx.var.http_origin
local path = ngx.var.uri
<pre class="brush:php;toolbar:false;"> -- 1. 提取租户ID或关键标识
local tenant_id = ngx.var.tenant_id or "default"
-- 2. 从 shared_dict 获取白名单(已预热)
local dict = ngx.shared.white_list
local rules = dict:get("rules_" .. tenant_id) or {}
-- 3. 检查 Origin 是否匹配任意规则
local allowed = false
for _, pattern in ipairs(rules) do
if match(origin, pattern, "jo") then
allowed = true
break
end
end
if not allowed then
ngx.status = 403
ngx.header["Content-Type"] = "text/plain"
ngx.say("Forbidden: Origin not whitelisted")
ngx.exit(ngx.HTTP_FORBIDDEN)
end
}
# 预检成功,返回标准 CORS 头
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers $http_access_control_request_headers;
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Max-Age 86400;
add_header Content-Length 0;
return 204;
}}
注意跨阶段协同与性能兜底
真实场景中需兼顾健壮性:
- 白名单加载失败(如 Redis 超时)时,默认放行或拒绝,需通过配置开关控制(如
ngx.shared.config:get("fail_open")) - 避免在
access_by_lua_block中做阻塞操作;DNS 解析、HTTP 请求等必须用 cosocket 异步方式 - 对高频 OPTIONS 请求启用
lua_shared_dict缓存结果,键可设计为"opt_" .. md5(origin .. path),减少重复计算 - 日志记录被拒绝的 Origin 和时间戳,便于审计,但避免打满磁盘(用
log_by_lua_block异步写)


















