记一次Nginx配置“CORS” 后, 服务端返回400导致跨域失效的的排查与修复(根因在 Nginx)
结论先行:归根结底是 Nginx 的头部添加策略不一致导致的。最终我在 Nginx 统一处理(含 always 与预检)解决了问题。
背景与时间线
- 我在一个前后端分离的项目里配置 CORS。外部流量进来先到 Nginx,再反向代理到 Java 后端。
- 我首先尝试在 Nginx 里配置跨域,结果表现为:
- 200 正常,跨域通过
- 400 时失败,浏览器拦截响应
- 为了验证问题来源,我随后改成在 Java 后端配置跨域。此时我发现:
- 直接访问后端端口(绕过 Nginx)时,200 与 400 都带有正确的 CORS 头
- 但是通过 Nginx 访问时,返回 200 的响应反而没有这些头,导致跨域失败
💡 上述验证结果清晰地表明:Java 后端设置的 CORS 头是对的,但经由 Nginx 的路径上,这些头在某些场景被“丢失/未添加”。根因在 Nginx。
现象复盘与对比
我用浏览器 DevTools 和 cURL 分别验证了“直接访问后端端口”和“经过 Nginx 代理”的差异。
- 直接访问后端端口:
- 预检 OPTIONS:带齐
Access-Control-Allow-*
头 - 实际请求(200/400):均带
Access-Control-Allow-Origin
等关键头
- 预检 OPTIONS:带齐
- 经 Nginx 访问:
- Nginx 管控时:200 正常、400 缺头
- Java 管控时:200 缺头、400 正常(因为从后端直接返回的错误路径带了头)
📊 小结对比(简化):
访问路径 | 状态码 200 | 状态码 400 |
---|---|---|
直接访问后端端口 | 有 CORS 头 ✅ | 有 CORS 头 ✅ |
经过 Nginx 代理(Nginx 配 CORS) | 有 CORS 头 ✅ | 无 CORS 头 ❌ |
经过 Nginx 代理(Java 配 CORS) | 无 CORS 头 ❌ | 有 CORS 头 ✅ |
这说明:Java 逻辑是正确的;问题根因出在 Nginx 代理路径对响应头的添加/传播不一致。
我最初的配置(导致问题的来源)
1) 我在 Nginx 的原始配置
location ~ /api/v1/.* {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "$connection_upgrade";
client_max_body_size 50m;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options ALLOWALL;
proxy_pass http://49.232.12.79:8088;
proxy_set_header Host 49.232.12.79:8088;
}
2) 我在 Java 后端的尝试(用于验证)
String reqHeader = request.getHeader("Access-Control-Request-Headers");
response.setStatus(HttpStatus.OK.value());
if (reqHeader != null){
response.setHeader("Access-Control-Allow-Headers", reqHeader);
} else {
StringBuilder headerNames = new StringBuilder();
request.getHeaderNames().asIterator().forEachRemaining(headerName -> {
if (!headerNames.isEmpty()){
headerNames.append(", ");
}
headerNames.append(headerName);
});
response.setHeader("Access-Control-Allow-Headers", headerNames.toString());
}
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
response.setHeader("Access-Control-Max-Age", "3600");
String origin = request.getHeader("Origin");
if (StringUtils.hasText(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);
}
if ("OPTIONS".equalsIgnoreCase(request.getMethod())){
response.setStatus(204);
return false;
}
直接访问后端端口时,这些头都在;通过 Nginx 时,返回 200 的响应看不到这些头,进一步说明 Nginx 环节出了问题。
根因分析(为何是 Nginx 的锅)🔍
-
add_header 的默认适用范围
Nginx 的add_header
在不加always
时,通常只对“成功类响应”(2xx/3xx)稳定生效。
这可以解释“在 Nginx 配置时,400 没有 CORS 头”的现象。 -
代理路径上的头传播与策略冲突
当我改为在 Java 添加 CORS 头时,直接访问后端端口有头,但经 Nginx 代理后 200 的响应不见了这些头。这往往出现在:- Nginx 层同时也在添加(或未添加)同名/相关头,导致在不同响应路径出现覆盖、缺失或不一致
- 预检与实际请求走了不同的处理分支(例如某些 if/错误页/拦截场景),从而头的添加不统一
- 没有
Vary: Origin
以及凭证策略不一致时,缓存或中间层行为让响应表现“忽有忽无”
总结一句:同一个代理层对不同响应码、不同路径的“加头策略不一致”,会导致“部分状态码有 CORS、部分没有”的诡异现象。
我通过“直接访问后端端口”验证了 Java 的头是正确生成的,问题只在经由 Nginx 时发生,因此归根结底是 Nginx 的配置与行为导致。
解决方案:在 Nginx 统一处理 CORS(含 always 与预检)✅
我最终选择在 Nginx 统一加 CORS,并去掉Java中的跨域配置, 确保所有响应码、所有路径一致,并把预检在边缘处理掉。同时对凭证场景回显具体 Origin,并加入 Vary: Origin
。
server {
# ... 其它 server 配置
location ~ /api/v1/.* {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "$connection_upgrade";
client_max_body_size 50m;
# 2) 预检请求统一处理
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers $http_access_control_request_headers always;
add_header Access-Control-Max-Age 86400 always;
add_header Vary "Origin" always;
return 204;
}
# 3) 实际请求的 CORS 头(所有响应码)
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Credentials true always;
add_header Vary "Origin" always;
# 4) 反向代理至后端
proxy_pass http://49.232.12.79:8088;
proxy_set_header Host 49.232.12.79:8088;
# 如确有需要:
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options ALLOWALL always;
}
}
📌 要点:
- 使用
always
保证 2xx/3xx/4xx/5xx 都带头- 回显具体 Origin 支持凭证;如无凭证可用
*
(但需与前端一致)- 预检就地返回,减少后端负担
- 加
Vary: Origin
,避免缓存污染- 如果使用了
error_page
或proxy_intercept_errors on;
,确保错误页也继承上述头
验证与结果
- 预检 OPTIONS:204,带
Access-Control-Allow-*
与Vary: Origin
- 实际请求:
- 200 响应:带 CORS 头,浏览器不再拦截
- 400 响应:仍带 CORS 头,前端能拿到错误信息并正常处理
从浏览器与 cURL 的验证看,问题彻底解决,行为稳定。
排查经验与检查清单 📋
- 在 Nginx 使用
add_header ... always
,否则 4xx/5xx 可能没有头 - 预检(OPTIONS)在边缘快速返回并带齐头
- 有凭证(Cookie/Authorization)时不能用
*
,需回显具体 Origin,并加Vary: Origin
- 避免“前后端同时加 CORS、策略不一致”,建议统一在一个层处理
- 若有自定义错误页或启用错误拦截,确保错误响应同样带 CORS 头
- 用“直接访问后端端口 vs 经过 Nginx”的方式来定位问题归属
- 用浏览器 DevTools 与 cURL 双重验证不同状态码下的头是否一致
我的结论与收获 🎯
- 这次的坑核心在于:Nginx 对响应头的添加在不同状态码与不同处理路径上不一致。这会造成“200 有、400 无”或“200 无、400 有”的反直觉现象。
- 通过“直接访问后端端口”的对比实验,我确认了 Java 后端的 CORS 头是正确生成的;问题只在经由 Nginx 代理时出现,因此根因在 Nginx。
- 最终我采用了“在 Nginx 统一配置 CORS(包含 always 与预检)”,让所有响应码和路径的行为保持一致,问题彻底解决。
如果你也遇到类似“跨域在部分状态码生效”的问题,强烈建议先比对“直连后端 vs 经 Nginx”的响应头,再检查 Nginx 的
add_header
是否统一(尤其是always
)、预检是否正确处理、以及是否存在策略冲突。这样能快速锁定问题归属,少走弯路。