跨域(CORS)¶
浏览器的同源策略看的是'协议+域名+端口'三元组,不是'同一台机器'——只要有一个不一样就跨域
核心概念¶
- 同源策略 — 浏览器的安全基石,不同源的请求一律先拦住
- Origin 三元组 — 协议 + 域名 + 端口,三者必须完全一致才算同源
- CORS — 跨源资源共享,服务器通过响应头告诉浏览器"允许谁来访问"
- 预检请求 — 非简单请求浏览器先发 OPTIONS 探路,确认安全后才发真正请求
详解¶
同源的定义¶
浏览器判断跨域的标准不是"是不是同一台机器",而是:
源(Origin)= 协议 + 域名 + 端口
https://example.com:443
↑ ↑ ↑
协议 域名 端口
三个都一样 → 同源 → 没问题
任何一个不一样 → 跨域 → 浏览器拦截
常见的"同一台主机但跨域"情况¶
1. 协议不同¶
2. 端口不同¶
前后端分离开发时极其常见。
3. 域名写法不同¶
4. 子域名不同¶
页面:https://app.example.com (前端)
请求:https://api.example.com (后端 API)
app.example.com ≠ api.example.com → 跨域!
图示¶
graph TD
subgraph 同源 ✅
A1["https://example.com:443"]
A2["https://example.com:443"]
A1 --- A2
end
subgraph 跨域 ❌
B1["https://app.example.com:443"]
B2["https://api.example.com:443"]
B1 --- B2
end
subgraph 跨域 ❌
C1["http://localhost:3000"]
C2["http://localhost:8080"]
C1 --- C2
end
subgraph 跨域 ❌
D1["http://example.com:80"]
D2["https://example.com:443"]
D1 --- D2
end
为什么要拦截?¶
sequenceDiagram
participant You as 你
participant Bank as bank.com
participant Evil as evil.com
Note over You,Bank: 你登录了银行网站,浏览器存了 cookie
You->>Evil: 访问恶意网站
Evil->>Bank: 请求 bank.com/api/transfer(冒充你转账)
Note over Bank: 浏览器同源策略拦截!
Note over Bank: evil.com 的请求被阻止 😅
同源策略 = 浏览器替你守门,不同源的请求一律先拦住。
解决方案¶
方案 1:后端设置 CORS 响应头(最常用)¶
# Python Flask
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app) # 允许所有来源
# 或精确控制
CORS(app, origins=["https://app.example.com"])
// Go 标准库
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://app.example.com")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
# Nginx
add_header Access-Control-Allow-Origin "https://app.example.com";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
方案 2:前端开发代理¶
// Vite 开发服务器配置
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
}
}
}
}
浏览器看到的请求始终在 localhost:3000,同源,不触发跨域。
方案 3:Nginx 反向代理(生产环境)¶
server {
listen 80;
server_name app.example.com;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://localhost:8080;
# 对浏览器:请求的是 app.example.com/api → 同源 ✅
}
}
简单请求 vs 预检请求¶
| 类型 | 条件 | 行为 |
|---|---|---|
| 简单请求 | GET/POST/HEAD + 简单 Header | 浏览器直接发,看响应头判断 |
| 预检请求 | PUT/DELETE/自定义 Header 等 | 浏览器先发 OPTIONS 探路,确认后再发 |
预检请求流程:
sequenceDiagram
participant C as 浏览器
participant S as 服务器
C->>S: OPTIONS /api/data(预检)
S->>C: 204 No Content
Note over S: Access-Control-Allow-Origin: https://app.example.com
Note over S: Access-Control-Allow-Methods: PUT, POST, GET
C->>C: 确认允许
C->>S: PUT /api/data(真正的请求)
S->>C: 200 OK
网络面板里一个请求出现两次——第一次是 OPTIONS 预检,不是 bug。
常见陷阱¶
Access-Control-Allow-Origin 不能设为 * 同时带 Cookie
如果请求携带 Cookie(withCredentials: true),Access-Control-Allow-Origin 不能是 *,必须指定具体来源。否则浏览器会拒绝。
忘记处理 OPTIONS 预检请求
很多后端框架默认不处理 OPTIONS 方法,导致预检请求返回 405 Method Not Allowed。需要显式添加 OPTIONS 处理逻辑。
练习题¶
为什么 WebSocket 不受同源策略限制?
答案
WebSocket 连接一旦建立,就是全双工通信,不受 CORS 限制。但建立连接的**握手阶段**(HTTP Upgrade 请求)仍然受同源策略影响。所以 WebSocket 的安全性依赖服务器端的身份验证,而不是浏览器的同源策略。
如果后端设置了 Access-Control-Allow-Origin: *,前端 withCredentials: true 会怎样?
答案
浏览器会拒绝。规范明确要求:当请求携带凭据(Cookie、HTTP Auth)时,Access-Control-Allow-Origin 不能是通配符 *,必须是具体的源。这是为了防止任意网站都能通过你浏览器里的 Cookie 访问受保护资源。