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

245 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [Nginx, 反向代理, 跨域, CORS, 运维]
create time: 2026-05-15 18:00
---
# Nginx 反向代理处理跨域
## 概述
本文档讲解如何利用 Nginx 反向代理解决浏览器端跨域(CORS)问题,从"最干净的方案"到"兜底方案"逐一展开,覆盖生产环境的典型场景。
> [!TIP] 核心思路
> 跨域是浏览器的安全策略,不是后端的限制。Nginx 做反向代理的核心思想:**让浏览器看到的地址和页面同源**,或者 **由 Nginx 在响应头中注入合法的 CORS header**。
## 正文
### 三种方案总览
```mermaid
flowchart LR
A[遇到跨域问题] --> B{能否统一域名?}
B -- 能 --> C["方案一: 同源代理<br/>零跨域, 最干净"]
B -- 不能 --> D{后端已设 CORS header?}
D -- 能控制后端 --> E["方案二: Nginx 注入<br/>网关统一管理"]
D -- 无法改后端 --> F["方案三: 透传 Header<br/>保留上游配置"]
```
---
### 方案一:同源代理 — 从根本上消灭跨域(推荐)
请求的域名和页面域名完全一致,浏览器根本不认为这是跨域请求。
```nginx
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;
}
}
```
**请求链路示意:**
```mermaid
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 网关",负责注入正确的响应头。
```nginx
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
# 如果你的 Nginx 配置里只有纯粹的 proxy_pass,无需额外操作:
location /api/ {
proxy_pass http://backend:8080;
}
```
但如果因为其他原因你已经在这个 `location` 里有 `add_header`(比如加了自定义 X-Custom-Header),就需要把 CORS header 一并补上:
```nginx
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 白名单中不包含这些字段,预检直接失败:
```bash
# 在控制台观察到的典型错误
# 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
```mermaid
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 中间件配置参考