This commit is contained in:
2026-05-24 11:42:38 +08:00
commit 30d312ac35
521 changed files with 146481 additions and 0 deletions
+389
View File
@@ -0,0 +1,389 @@
---
tags: [CORS, 跨域, 安全, HTTP, Web开发, 前后端分离]
create time: 2026-05-18 14:30
---
# 企业项目跨域问题解决方案
## 概述
跨域(Cross-Origin Resource Sharing, CORS)是前后端分离架构中最常见的网络限制问题。本文从浏览器同源策略出发,系统讲解跨域产生的原理、常见场景及完整解决方案,涵盖服务端配置、Nginx 反向代理和客户端降级方案,并附带 Go / Spring Boot / Node.js / Nginx 的实际代码示例。
> [!question] 思考:为什么浏览器要阻止跨域请求?
>
> 根源在于浏览器的「同源策略」(Same-Origin Policy)——它是一种安全机制,防止恶意网站读取你当前页面的 Cookie、LocalStorage 和 API 响应。但这一保护也带来了开发的困扰:**我们需要在保持安全的前提下,让不同域的服务之间正常通信。**
## 什么是跨域
### 同源的判定
两个 URL 只要 **协议(protocol)**、**域名(host)**、**端口(port)** 三者完全一致,就称为同源。任意一项不同,即为跨域。
| 当前 URL | 目标 URL | 是否跨域 | 原因 |
|----------|----------|----------|------|
| `https://api.example.com` | `https://app.example.com` | ✅ 跨域 | 域名不同 |
| `http://localhost:3000` | `http://localhost:8080` | ✅ 跨域 | 端口不同 |
| `https://example.com` | `http://example.com` | ✅ 跨域 | 协议不同 |
| `https://example.com/api` | `https://example.com/data` | ❌ 同域 | 仅路径不同(不算跨域) |
### 跨域的本质
跨域不是请求发不出去,而是 **浏览器拒绝接收响应**。实际过程如下:
```mermaid
flowchart TD
A["前端发起 AJAX / Fetch 请求"] --> B{"是否为跨域请求?"}
B -- '否' --> C["正常发送, 浏览器接收响应"]
B -- '是' --> D{"是否预检请求?"}
D -- '简单请求' --> E["直接发送请求"]
D -- '复杂请求' --> F["先发 OPTIONS 预检"]
F --> G{"服务端返回 200 + 正确 CORS 头?"}
G -- '是' --> E
G -- '否' --> H["浏览器拦截预检, 不发送真实请求"]
E --> I{"服务端是否携带 CORS 响应头?"}
I -- '是' --> C
I -- '否' --> J["浏览器拒绝响应, 控制台报错"]
```
关键结论:**服务器必须设置正确的 CORS 响应头,浏览器才会放行响应给 JavaScript。**
## 常见跨域场景
### 开发环境(本地 vs 后端)
```mermaid
flowchart LR
DevBrowser["浏览器<br/>localhost:5173"] --> Backend["Go API :8080<br/>(请求 /api/users)"]
```
这是开发阶段最常见的跨域场景——前端 dev server 跑在 `localhost:5173`,后端服务在 `:8080`,端口不同即触发跨域。
### 生产环境(多子域名)
```mermaid
flowchart LR
App["app.company.com"] --> Api["api.company.com<br/>(需配 CORS)"]
```
主站和 API 属于不同子域名,同样需要处理跨域。
### 嵌入场景(iframe / CDN)
- 页面加载第三方 CDN 上的静态资源(字体、JS、CSS)→ 通常没问题(GET 请求天然不受限)
- 页面内 iframe 向自身域名发 XHR 请求 → 受跨域限制
- 调用第三方 API(如支付回调、地图 SDK)→ 取决于对方是否支持 CORS
## 解决方案
### 方案一:服务端配置 CORS(推荐首选)
**适用场景**:自有服务端,可直接修改响应头。
#### Go (Gin 框架)
使用官方 `gin-contrib/cors` 中间件:
```go
import "github.com/gin-contrib/cors"
router := gin.Default()
config := cors.DefaultConfig()
config.AllowOrigins = []string{"https://app.example.com"} // 明确允许的源
config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"}
config.AllowHeaders = []string{"Origin", "Content-Type", "Authorization"}
config.AllowCredentials = true // 携带 Cookie 时必须设为 true
config.MaxAge = 12 * time.Hour // 预检请求缓存时长
router.Use(cors.New(config))
```
`AllowOrigins` 白名单是安全的第一道防线——它告诉浏览器"只有这些域名可以读取我的接口数据"。`AllowCredentials` 用于允许携带 Cookie(如登录态),但配合白名单使用,绝不可与通配符 `*` 同时出现。`MaxAge` 设置预检请求在浏览器的缓存时长,减少不必要的 OPTIONS 请求。
> [!tip] 生产环境安全要点
>
> - **不要用 `AllowOrigins = ["*"]` 配合 `AllowCredentials = true`**,这会引发 panic 或无效配置。
> - 应显式列出允许的前端域名白名单,避免恶意站点劫持接口。
> - 如需支持多个前端域名,可从环境变量或配置中心动态读取。
#### Java (Spring Boot)
Spring Boot 提供了多种 CORS 配置方式:
**全局配置(实现 WebMvcConfigurer):**
```java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 生效的路径
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600); // 预检缓存 1 小时
}
}
```
**注解级别(细粒度控制单个 Controller):**
```java
@CrossOrigin(origins = "https://app.example.com", maxAge = 3600)
@RestController
@RequestMapping("/api/admin")
public class AdminController { ... }
```
#### Node.js (Express)
```js
const cors = require('cors');
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
```
此配置传入 Express 的中间件链中,会对所有路由生效。如需限定路径范围,可改为 `app.use('/api', cors(config))` 。与 Go / Java 方案相比,Node.js 方案最轻量——无需额外注解或配置类,只需在启动时注册一次即可。
### 方案二:Nginx 反向代理(零侵入)
**适用场景**:无法修改后端代码、多语言混合后端统一治理、隐藏后端地址提升安全。
核心思路:让前端访问 Nginx 的 `/api` 路径,Nginx 把请求转发到后端,同时注入 CORS 响应头。
```nginx
server {
listen 443 ssl;
server_name app.example.com;
# --- 前端静态资源 ---
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
# --- Location 1: OPTIONS 预检请求(直接返回,不通后端)---
location ~ ^/api/.*$ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}
# --- Location 2: 真实请求(反向代理 + CORS 头)---
location /api/ {
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
proxy_pass http://backend_server:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
> [!warning] Nginx 注意事项
>
> - `if ($request_method = 'OPTIONS')` 中的 `if` 在 Nginx 中有陷阱,但它在这里是安全的用法(只操作 header)。
> - `always` 参数确保即使错误响应(4xx / 5xx)也带上 CORS 头。
> - `return 204` 直接返回空体,避免 OPTIONS 请求打到后端增加负载。
### Nginx 方案的架构图
```mermaid
flowchart TD
Browser["浏览器 app.example.com"] --> Nginx["Nginx 443<br/>(GET /api/users)"]
Nginx --> Backend["后端 :8080<br/>(proxy_pass)"]
Backend --> JSONResponse["JSON 响应"]
Nginx --> BrowserReply["返回含 CORS 头的<br/>JSON 响应"]
BrowserReply --> Browser
```
对比之下,不加 Nginx 时的直连模式:
```mermaid
flowchart TD
Browser["浏览器<br/>app.example.com<br/><b>❌ 跨域 + 无 CORS 头</b>"] --> Backend["后端 api.example.com:8080"]
```
此时浏览器直接访问后端接口,因域名和端口均不同触发跨域限制,且服务端未配置 CORS 头,浏览器拦截响应。
### 方案三:开发期 Vite/Webpack Proxy(开发专用)
**适用场景**:本地开发时绕过跨域,上线后由 Nginx 处理。
#### Vite 配置 (`vite.config.ts`)
```ts
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080', // 后端地址
changeOrigin: true, // 改写 Host 头
secure: false, // 自签证书时可关闭校验
},
},
},
});
```
这样前端发 `fetch('/api/users')` 时,Vite dev server 会转发到 `http://localhost:8080/api/users`,因为是同源请求,不存在跨域。
#### Webpack (`vue.config.js` / `webpack.config.js`)
```js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
};
```
`changeOrigin: true` 会将被代理请求的 Host 头改为后端地址,这对于某些依赖 Host 做鉴权的后端服务是必要的。Webpack proxy 同样只在本地开发生效,生产环境仍需 Nginx 或 CORS 中间件配合。
> [!info] Vite/Webpack Proxy 与正式方案的区别
>
> | | 开发 Proxy | Nginx / 服务端 CORS |
> |---|---|---|
> | 用途 | 仅限本地开发 | 生产环境 |
> | 需要后端改吗 | 不需要 | 不需要(Nginx)/ 需要(服务端) |
> | HTTPS 支持 | 需额外配置 | 原生支持 |
> | 部署复杂度 | 无 | 低 |
### 方案四:JSONP / 服务端重定向(历史方案,了解即可)
#### JSONP
利用 `<script>` 标签不受同源策略限制的特性,通过回调函数接收数据。仅支持 GET,且存在 XSS 风险,已属历史方案。
**原理**:后端返回 `callbackName(data)` 这样的 JavaScript 代码,前端动态插入 `<script>` 标签触发执行。
```html
<!-- 前端 -->
<script>
function jsonpCallback(data) {
console.log(data); // 拿到后端返回的数据
}
</script>
<script src="https://api.example.com/data?callback=jsonpCallback"></script>
```
```go
// Go 后端示例
func JSONPHandler(w http.ResponseWriter, r *http.Request) {
callback := r.URL.Query().Get("callback")
data := `{"name": "John", "age": 30}`
w.Header().Set("Content-Type", "application/javascript")
fmt.Fprintf(w, "%s(%s)", callback, data)
}
```
> [!note] JSONP 的局限性
>
> - **只支持 GET**——POST/PUT/DELETE 等无法实现
> - **无错误处理**——脚本加载失败时只能通过 `onerror` 粗略捕获
> - **安全风险**——任何提供 JSONP 的接口都可能被恶意站点调用,需额外做 Referer 校验
> - **已被 CORS 全面取代**——现代浏览器全部支持 CORS,新项目不应考虑此方案
#### 服务端重定向
后端自己调后端,前端只请求同源接口。适合微服务间调用,但不解决浏览器侧跨域。
## 进阶话题
### 带凭据的请求(Cookie / Authorization Header)
当 `withCredentials: true`(fetch)或 `xhr.withCredentials = true`(XMLHttpRequest)时:
1. 服务端 `Access-Control-Allow-Origin` **不能**为 `*`,必须是具体域名
2. 服务端需设置 `Access-Control-Allow-Credentials: true`
3. 浏览器会在请求中带上 Cookie
```ts
// 前端 fetch 带 Cookie
fetch('https://api.example.com/user/profile', {
method: 'GET',
credentials: 'include', // 关键:带上 Cookie
headers: { 'Authorization': 'Bearer xxx' },
});
```
`credentials: 'include'` 是让浏览器发送 Cookie 的关键。如果只设 `credentials` 而不配服务端 `Access-Control-Allow-Credentials: true`,或者后端 `Allow-Origin` 仍为 `*`,请求同样会被拦截——这是一个"双方都满足才能通过"的条件。
### 预检请求的优化
复杂请求(自定义头、PUT/DELETE 等)会先发送 `OPTIONS` 预检。可通过以下方式减少开销:
#### Max-Age 缓存的影响
```mermaid
flowchart LR
A["Max-Age = 0<br/><b>每次请求都发 OPTIONS</b>"] --> B["⚠️ 高延迟,<br/>服务器压力大"]
C["Max-Age = 86400<br/><b>24小时内复用预检结果</b>"] --> D["✅ 首慢后续快,<br/>负载低"]
```
#### 设置方式
服务端或 Nginx 中设置响应头即可:
- `Access-Control-Max-Age: 86400` — 缓存 24 小时,超过需重新预检
- 一般建议设为 **1 天到 7 天**,具体取决于接口变更频率
### RESTful 动词与预检的关系
| 方法 | 是否可能预检 | 原因 |
|------|-------------|------|
| GET | ❌ 不会 | 简单请求 |
| POST (application/x-www-form-urlencoded / multipart/form-data / text/plain) | ❌ 可能不会 | Content-Type 在白名单内 |
| POST (application/json) | ✅ 会预检 | Content-Type 不在简单请求白名单 |
| PUT / DELETE / PATCH | ✅ 会预检 | 不在简单请求方法的白名单 |
### 常见问题排查清单
> [!check] 跨域问题排查流程
>
> 1. **确认跨域类型** — 打开浏览器 DevTools → Network,看请求状态码是 `200` 还是 `cors error`
> 2. **检查请求头** — Request Headers 里是否有 `Origin`,值是否正确
> 3. **检查响应头** — Response Headers 中是否有 `Access-Control-Allow-Origin`,值和前端域名匹配吗
> 4. **区分简单请求和预检** — 有无单独的 `OPTIONS` 请求发出?OPTIONS 返回什么?
> 5. **credentials 场景** — 是否同时满足了 `具体 Allow-Origin` + `Allow-Credentials: true`
> 6. **多级代理链路** — 经过网关 / WAF / CDN 时,确认每一层都不删除 CORS 头
## 方案选型速查表
| 场景 | 推荐方案 | 理由 |
|------|----------|------|
| 前后端同公司,后端可改 | 服务端 CORS 中间件 | 最直接,语义清晰 |
| 多语言后端 / 不便改代码 | Nginx 反代注入 CORS 头 | 统一治理,零侵入 |
| 本地开发 | Vite/Webpack Proxy | 开发体验最佳 |
| 生产环境隐藏后端 IP | Nginx 反代 | 安全 + 跨域双收益 |
| 纯静态页调用第三方公开 API | 让第三方配好 CORS | 自身无能为力 |
## 关联笔记
- [[hhs/GIN/3-middleware/cors-registration-scope/cors-preflight]] — Gin 框架下预检请求的详细机制
- [[hhs/DEV/Nginx/Nginx]] — Nginx 配置速查,含反向代理、负载均衡等
- [[hhs/DEV/XSS与CSRF攻击/XSS与CSRF攻击]] — 同源策略是防御 XSS/CSRF 的基础
- [[hhs/KingSoft/docs/Go语言web开发/03-服务端鉴权认证方案]] — Cookie / Session 鉴权与 CORS 凭据的配合
- [[hhs/MS/02-服务治理/01-API网关]] — 微服务架构中通过 API 网关统一处理跨域