全文默認講的是瀏覽器端發起的 HTTP 請求的“跨域”問題(同源策略導致的受限)。
跨域 / 同源策略概述
- **同源(same-origin)**:協議、域名(host)、端口 三者完全相同稱為同源。 例如
https://example.com:443和http://example.com不是同源(協議不同)。 - **同源策略(SOP)**:瀏覽器的一種安全機制,限制從一個源加載的腳本去讀取另一個源的響應(以防 CSRF / 數據泄露)。
- **跨域(cross-origin)**:當請求目標不滿足同源時即為跨域,請求仍可發出,但瀏覽器會阻止 JS 讀取響應,除非服務器明確允許(即 CORS -> 跨域資源共享)。
CORS(Cross-Origin Resource Sharing)
CORS(Cross-Origin Resource Sharing)跨域資源共享。瀏覽器會根據響應頭判斷是否允許跨域讀取。一些關鍵的響應頭包括:
Access-Control-Allow-Origin:允許的源(或*)。但是這裏我們一般不配置為
*,因為如果響應包含敏感數據或依賴 cookie/憑證(Authorization / session),*與Access-Control-Allow-Credentials: true不能同用,瀏覽器也會拒絕這種組合,屬於安全漏洞 ⚠️。(篇幅有限,更多細節見下篇文章。)Access-Control-Allow-Methods:允許的方法(GET, POST, PUT...)。Access-Control-Allow-Headers:允許的自定義 Header(如Authorization, X-Custom-Header)。Access-Control-Allow-Credentials:是否允許帶 cookie/憑證(true表示允許)。Access-Control-Expose-Headers:允許前端訪問的響應頭。Access-Control-Max-Age:預檢(preflight)結果緩存時長(秒)。預檢請求(preflight):當請求使用了非“簡單請求”方法或自定義了 header、或
Content-Type非簡單類型時,瀏覽器會先發OPTIONS請求詢問服務器是否允許。
常見解決方案
方案 A — 在服務端正確配置 CORS
這是最通用也最推薦的做法:在響應裏返回正確的 CORS 頭。
Express(Node)示例(使用 cors 中間件)
const express = require('express')
const cors = require('cors')
const app = express()
app.use(cors({
origin: 'https://app.example.com', // 注意不能用 '*' 配合 credentials
methods: ['GET','POST','PUT','DELETE','OPTIONS'],
credentials: true, // 允許 cookie
allowedHeaders: ['Content-Type','Authorization','X-Requested-With']
}));
Nginx:把 CORS header 加到響應上
location /api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE';
add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 把 header 添加到後端響應
proxy_hide_header 'Access-Control-Allow-Origin';
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Credentials' 'true';
}
方案 B — 前端代理到同源(僅開發階段)
開發階段或沒有後端權限時使用我們經常使用構建工具的 dev server 實現反向代理。把跨域請求代理到本地 dev server(同源),由 dev server 轉發到目標服務器。
Vite dev server 配置
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
}
方案 C — Nginx 反向代理(生產階段常用)
這也是生產環境下的常用方案。在 Nginx 等反向代理層統一轉發,前端調用同域(Nginx),Nginx 代理後再與後端跨域通信。
前端請求:
https://app.example.com/api/...(同源) Nginx proxy_pass ->https://api.example.com/...。
server {
listen 80;
server_name example.com;
# 靜態資源代理
location /static/ {
root /project/static;
index index.html index.html; # 訪問 /static/ 會自動“重定向”到 /project/static/index.html 文件
}
# API代理
location /api/ {
rewrite ^/api/(.*)$ /$1 break; # 如果服務器沒有 /api 記得重寫
proxy_pass http://api.example.com; # 代理的地址
}
}
方案 D — JSONP(已過時,僅限 GET)
只支持 GET,通過 <script> 標籤繞過 SOP,服務端返回 callback(...),容易被攻擊者使用 callback 惡意函數做 XSS 攻擊。
方案 E — postMessage(跨窗口/iframe 場景)
當需要跨域頁面間通信(iframe 和父窗口),使用 window.postMessage。適用於頁面間數據交換,不適用於普通 API 請求。我們一般會在微前端、OAuth 第三方登錄、單點登錄、支付頁面回調、WebView 混合開發中使用。
方案 F — WebSocket(不受 CORS 限制)
WebSocket 握手不是標準的 CORS;如果用 WS/WSS,瀏覽器不會因同源限制阻止讀取消息(但服務器可能做 origin 校驗)。我們也不可能為了跨域強行使用 WebSocket。(之後會詳細介紹 HTTP 協議和 WebSocket 協議關係和他們的使用)
開發時一些坑
在我剛開始獨立從零到一搭建前後端項目的時候(當時還沒有什麼 AI Coding,全憑一手搜索引擎),這個問題讓我紅温到晚上一點也沒有解決(沒錯,當時我還很菜 hh)。
處理帶 cookie / 帶憑證的跨域請求
- 服務端:
Access-Control-Allow-Origin: https://app.example.com(具體 origin,不能是*)Access-Control-Allow-Credentials: true
- 前端(fetch / xhr):
fetch(url, { credentials: 'include' })或xhr.withCredentials = true
當時我犯的錯誤:
- 沒有設置
credentials: 'include',瀏覽器不會發送 cookie。 - 服務端回送
Access-Control-Allow-Origin: *與Allow-Credentials: true同時存在(瀏覽器會拒絕)。