245 lines
10 KiB
Markdown
245 lines
10 KiB
Markdown
---
|
||
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 中间件配置参考
|