10 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
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_passURL 末尾有没有/,决定了 location 匹配部分是否会被截断。
- 没
/→ 保持前端路径前缀不变(适合前后端共用同一个 API 前缀)- 有
/→ 自动去掉 location 前缀(适合后端路由不需要前缀的场景)
陷阱二:CORS header 突然消失 — Nginx 的继承规则
Nginx 的 add_header 有一个非常反直觉的行为:一旦你在某个层级定义了 add_header,该层级及以下会丢弃所有父级的 header + 上游响应头。
排查方法:
- 用
curl -vI https://api.example.com/api/test直接看响应头 - 如果能看到 CORS header → 是 Nginx 配置层覆盖问题,回到方案三的规则修复
- 如果看不到 → 检查后端实际返回了哪些 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 支持 | ✅ 天然支持 | ✅ 配合白名单可用 | ✅ 天然支持 |
| 维护成本 | 低 | 中等(域名变更需改配置) | 最低 |
| 推荐优先级 | 🥇 首选 | 🥈 次选 | 🥉 兜底 |
关联笔记
- hzh/DEV/跨域问题调试 — 跨域排查完整指南
- GIN/3-middleware/cors-registration-scope — 后端 Go/Gin 端 CORS 中间件配置参考