Files
cs-note/hzh/DEV/跨域问题调试/nginx-reverse-proxy-cors.md
T
2026-05-24 11:42:38 +08:00

10 KiB
Raw Blame History

tags, create time
tags create time
Nginx
反向代理
跨域
CORS
运维
2026-05-15 18:00

Nginx 反向代理处理跨域

概述

本文档讲解如何利用 Nginx 反向代理解决浏览器端跨域(CORS)问题,从"最干净的方案"到"兜底方案"逐一展开,覆盖生产环境的典型场景。

[!TIP] 核心思路 跨域是浏览器的安全策略,不是后端的限制。Nginx 做反向代理的核心思想:让浏览器看到的地址和页面同源,或者 由 Nginx 在响应头中注入合法的 CORS header。

正文

三种方案总览

flowchart LR
    A[遇到跨域问题] --> B{能否统一域名?}
    B -- 能 --> C["方案一: 同源代理<br/>零跨域, 最干净"]
    B -- 不能 --> D{后端已设 CORS header?}
    D -- 能控制后端 --> E["方案二: Nginx 注入<br/>网关统一管理"]
    D -- 无法改后端 --> F["方案三: 透传 Header<br/>保留上游配置"]

方案一:同源代理 — 从根本上消灭跨域(推荐)

请求的域名和页面域名完全一致,浏览器根本不认为这是跨域请求。

server {
    listen 80;
    server_name app.example.com;

    # 前端静态资源
    location / {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /index.html;
    }

    # API 代理 — 浏览器看到 /api/xxx,和页面同源
    location /api/ {
        proxy_pass http://backend: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;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

请求链路示意:

sequenceDiagram
    participant B as 浏览器
    participant N as Nginx
    participant S as 后端服务

    B->>N: GET /api/user (同源,无预检)
    N->>S: GET /api/user
    S-->>N: 200 OK (body)
    N-->>B: 200 OK (body,不含 CORS header)
维度 说明
是否真正解决跨域 ✅ 根本解决,浏览器不认为存在跨域
Cookie / Session 天然支持,无需额外配置
安全性 最高,不存在任何跨域风险
适用前提 你能控制前端部署域名

思考:如果前端和后端分别部署在不同的子域名(如 app.example.com 和 api.example.com),这个方案还适用吗?不适用——这就是下面两种方案的用武之地。


方案二:Nginx 注入 CORS header(前后端无法同域时)

当 API 确实需要独立域名时,Nginx 充当"CORS 网关",负责注入正确的响应头。

server {
    listen 80;
    server_name api.example.com;

    # 允许的合法前端域名列表
    set $cors_origin "";

    if ($http_origin ~* "^https://(app\.example\.com|dev\.example\.com)$") {
        set $cors_origin $http_origin;
    }

    location /api/ {
        proxy_pass http://backend:8080;

        # 白名单校验后的 Origin(未匹配则不注入,防止随意跨域)
        add_header Access-Control-Allow-Origin $cors_origin always;
        add_header Access-Control-Allow-Credentials "true" always;
        add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Token" always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
        add_header Access-Control-Max-Age "3600" always;
        add_header Access-Control-Expose-Headers "X-Total-Count, X-Request-Id" always;

        # 预检请求直接响应 204
        if ($request_method = OPTIONS) {
            return 204;
        }
    }
}

为什么这里用了正则白名单而不是直接回显 $http_origin?

因为直接回显虽然省事,但意味着任意域名都能通过白名单校验拿到 CORS header,等同于变相开放。通过 ~* 正则限定具体域名,是安全加固的一层。

关键参数解释

参数 作用 注意事项
add_header ... always always 确保即使后端返回 4xx/5xx 也加上 CORS header 不加 always 时错误响应会丢失跨域信息
set $cors_origin "" 初始化为空,默认不允许跨域 这是一种"拒绝所有、放行白名单"的安全策略
if ($request_method = OPTIONS) 拦截预检请求,避免转发给后端 Nginx 的 if 在此处使用是安全的(官方推荐用法)
Access-Control-Max-Age 浏览器缓存预检结果的时间(秒) 开发时建议设为 0,生产可设 3600~86400

[!WARNING] Credential 模式陷阱 如果设置了 Access-Control-Allow-Credentials: true,Access-Control-Allow-Origin 绝对不能是 *。必须动态回显具体域名或走白名单——上面的配置已经通过白名单规避了这个问题。


方案三:透传后端 CORS header

当后端已经正确配置了 CORS header 时,默认情况下 Nginx 不会移除这些响应头,所以大多数场景你不需要任何额外配置。

[!TIP] Nginx Header 传递规则(重点)

  • 无 add_header 时:Nginx 完整透传上游响应头(包括 CORS header),且子级 location 会继承父级的 add_header。
  • 有 add_header 时:Nginx 丢弃所有父级 add_header + 上游的 CORS header。此时必须显式重新添加需要的头。

这是最常见的坑——你在某个 location 里加了一个自定义 header,结果发现 CORS header 突然消失了。

# 如果你的 Nginx 配置里只有纯粹的 proxy_pass,无需额外操作:
location /api/ {
    proxy_pass http://backend:8080;
}

但如果因为其他原因你已经在这个 location 里有 add_header(比如加了自定义 X-Custom-Header),就需要把 CORS header 一并补上:

location /api/ {
    proxy_pass http://backend:8080;

    # 原有自定义 header
    add_header X-Request-Id $request_id always;

    # 补充 CORS header(否则会被 Nginx 吞掉)
    proxy_hide_header Access-Control-Allow-Origin;
    if ($http_origin ~* "^https://.+\.example\.com$") {
        add_header Access-Control-Allow-Origin $http_origin always;
        add_header Access-Control-Allow-Credentials "true" always;
    }
}
做法 效果 适用场景
不加任何 header ✅ 完全透传后端 CORS 后端已配置 CORS,Nginx 只做纯代理
仅加 proxy_hide_header ⚠️ 不发送 CORS header 需要临时屏蔽后端的 CORS
加 add_header + proxy_hide_header ✅ 手动重建 CORS 已在该 location 有其他 header 需共存时

[!NOTE] 最佳实践建议 如果后端和前端都在你的控制范围内,统一将 CORS 配置放在后端中间件层是最推荐的做法。Nginx 只负责路由转发,不做跨域决策——这样配置清晰、易于维护和审计。只有当你无法修改后端代码时,才需要在 Nginx 层"代劳"设置 CORS header(即回到方案二)。


常见陷阱与调试技巧

陷阱一:proxy_pass 尾部斜杠的语义差异

location /api/ 和 proxy_pass URL 末尾的 / 组合会产生不同行为,这是最容易踩的坑。

location proxy_pass 结尾 请求 /api/user → 转发给后端的路径
/api/ 无 /(http://backend:8080) /api/user(原样转发)
/api/ 有 /(http://backend:8080/) /user(/api/ 被截断替换)

记忆口诀:proxy_pass URL 末尾有没有 /,决定了 location 匹配部分是否会被截断。

  • 没 / → 保持前端路径前缀不变(适合前后端共用同一个 API 前缀)
  • 有 / → 自动去掉 location 前缀(适合后端路由不需要前缀的场景)

陷阱二:CORS header 突然消失 — Nginx 的继承规则

Nginx 的 add_header 有一个非常反直觉的行为:一旦你在某个层级定义了 add_header,该层级及以下会丢弃所有父级的 header + 上游响应头。

排查方法:

  1. 用 curl -vI https://api.example.com/api/test 直接看响应头
  2. 如果能看到 CORS header → 是 Nginx 配置层覆盖问题,回到方案三的规则修复
  3. 如果看不到 → 检查后端实际返回了哪些 header(可能后端根本没设)

陷阱三:Access-Control-Allow-Headers 遗漏字段

浏览器预检时会带上自定义 header(如 X-Token, Authorization),如果 Nginx 白名单中不包含这些字段,预检直接失败:

# 在控制台观察到的典型错误
# Access to XMLHttpRequest at '...' from origin '...' has been blocked by CORS policy:
# Request header field x-token is not allowed by Access-Control-Allow-Headers in preflight response.

解决:确保 Access-Control-Allow-Headers 包含前端所有可能发送的自定义 header。

调试 Checklist

flowchart TD
    A[浏览器报 CORS 错误] --> B{curl 能正常访问?}
    B -- No --> C["检查后端服务是否存活<br/>及健康检查路径"]
    B -- Yes --> D{"Response Header 有 CORS?"}
    D -- No --> E["后端未设置 CORS header<br/>→ 在后端中间件中添加"]
    D -- Yes --> F{"curl 有但浏览器没有?"}
    F -- Yes --> G["Nginx add_header 覆盖了上游<br/>→ 检查方案三的继承规则"]
    F -- No --> H["检查 Response Status Code<br/>是否为 2xx/3xx?<br/>(4xx/5xx 需加 always)"]

对比总结

维度 方案一(同源代理) 方案二(Nginx 注入) 方案三(透传后端)
是否真正解决跨域 ✅ 根本解决 ⚠️ 表面绕过 ⚠️ 取决于后端
安全性 最高(无跨域) 高(白名单可控) 低(信任后端)
Cookie 支持 ✅ 天然支持 ✅ 配合白名单可用 ✅ 天然支持
维护成本 低 中等(域名变更需改配置) 最低
推荐优先级 🥇 首选 🥈 次选 🥉 兜底

关联笔记