nginx 的 options 预检请求不携带业务逻辑,必须严格隔离于签名校验,仅由 nginx 直接返回 204;签名校验只针对非 options 请求;需精准控制 cors 头、动态校验 origin,并拆分 location 实现预检与主请求完全隔离。

Nginx 的 OPTIONS 预检请求本身不携带业务逻辑(比如签名参数、token 或自定义签名校验头),它只是浏览器发起的“探路请求”,用于确认后续真实请求是否被允许。所以预检不会绕过签名校验——关键在于:你不能让预检和真实请求共用同一套校验逻辑,更不能把签名校验放到预检响应里。
真正要防的是:有人伪造 OPTIONS 请求试探你的接口边界,或利用配置疏漏让非法 Origin / Headers 通过预检,从而为后续带签名的真实请求打开通道。因此配置重点是「严格隔离预检与主请求」+「精准控制 CORS 头」+「不透传签名校验逻辑到预检」。
✅ 确保预检不触发签名校验逻辑
签名校验(如 X-Signature、X-Timestamp、Authorization 等)只应在真实请求(GET/POST/PUT 等)中由后端或 Nginx 模块(如 nginx-http-sig-verify)校验。
OPTIONS 请求必须 完全跳过校验环节,由 Nginx 直接拦截并返回 204 —— 这样既快又安全,也避免签名校验模块误判空请求体导致失败。
正确做法:
- 不在
location /api/块中对所有请求统一做签名校验; - 把签名校验逻辑放在非 OPTIONS 的 location 或 if 判断内;
- 或使用
limit_except显式排除 OPTIONS:
location /api/ {
# 只对非 OPTIONS 方法启用签名校验(示例伪逻辑)
if ($request_method != 'OPTIONS') {
# 此处可嵌入 lua 或调用 auth 模块校验 X-Signature 等
set $need_auth 1;
}
if ($need_auth) {
# 调用签名校验逻辑(如 via lua-resty-jwt 或 custom module)
# 若失败,return 401
}
proxy_pass http://backend;
}
⚠️ 注意:Nginx 原生不支持复杂签名验签,通常需 Lua 或第三方模块;但无论用哪种方式,都不能在 OPTIONS 分支里执行它。
✅ 严格限制 Access-Control-Allow-Headers,防止签名头被滥用
浏览器在预检时会带上 Access-Control-Request-Headers,例如:
Access-Control-Request-Headers: X-Signature, X-Timestamp, Authorization
Nginx 必须显式、精确地在响应中声明允许哪些头,否则预检失败:
add_header 'Access-Control-Allow-Headers' 'X-Signature,X-Timestamp,Authorization,Content-Type,X-Requested-With' always;
- ✅ 列出所有前端实际使用的自定义头(含签名相关头),逗号分隔、无空格;
- ❌ 不要用
*(通配符)——它在含Credentials时无效,且无法配合X-Signature这类敏感头; - ❌ 不反射
Access-Control-Request-Headers的值(存在安全风险,可能放大攻击面)。
✅ 动态校验 Origin,拒绝非法来源的预检通行
如果接口要求带签名,通常也要求来源可信。不能简单写 Access-Control-Allow-Origin: *:
map $http_origin $cors_origin {
~*^https?://(admin\.example\.com|app\.mycompany\.io|localhost:3000)$ $http_origin;
default "";
}
server {
location /api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'X-Signature,X-Timestamp,Authorization,Content-Type' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' '86400' always;
add_header 'Cache-Control' 'public, max-age=86400' always;
return 204;
}
}
}
- 只有匹配白名单的 Origin 才获得
Access-Control-Allow-Origin响应头; - 不匹配则不设该头 → 浏览器直接拒绝后续真实请求(哪怕签名正确);
-
Access-Control-Allow-Credentials: true和非*的 Origin 必须同时满足,否则跨域带 cookie / 签名头会失败。
✅ 隔离预检路径,避免干扰主代理链
不要在一个 location 里混写 if ($request_method = 'OPTIONS') { ... return 204; } 和 proxy_pass:
# ❌ 错误:if + proxy_pass 同块易引发变量丢失、502 等问题
location /api/ {
if ($request_method = 'OPTIONS') { return 204; }
proxy_pass http://backend;
}
✅ 推荐拆成两个 location,且 OPTIONS 的必须前置:
# 1. 专用于预检(优先匹配)
location ~ ^/api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'X-Signature,X-Timestamp,Authorization,Content-Type' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' '86400' always;
add_header 'Cache-Control' 'public, max-age=86400' always;
return 204;
}
}
# 2. 主请求代理(不处理 OPTIONS)
location /api/ {
# 此处放签名校验、proxy_pass、超时等
proxy_pass http://backend;
}
这样确保 OPTIONS 绝对不走到后端,也不受任何中间逻辑(如 rewrite、auth、限流)影响。
不复杂但容易忽略。











