记一次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 等关键头
  • 经 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_pageproxy_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)、预检是否正确处理、以及是否存在策略冲突。这样能快速锁定问题归属,少走弯路。

posted @ 2025-10-10 11:04  Only丿阿海  阅读(2)  评论(0)    收藏  举报