計算機“十萬個為什么”之跨域
本文是計算機“十萬個為什么”系列的第五篇,主要是介紹跨域的相關知識。
作者:無限大
推薦閱讀時間:10 分鐘
一、引言:為什么會有跨域這個“攔路虎”?
想象你正在參觀一座戒備森嚴的城堡 🏰
🚪 城堡大門 = 瀏覽器安全機制
📜 訪客通行證 = 同源策略
🔄 沒有通行證卻想進入其他城堡的訪客 = 跨域請求
在 Web 世界中,跨域就像城堡之間的訪問限制,是瀏覽器為保護用戶數據安全而設置的重要防線。但為什么需要這樣的限制?當我們訪問不同網站時到底發生了什么?這篇文章將帶你深入探索跨域的奧秘,從基礎概念到高級解決方案,全面理解這個 Web 開發中不可避免的技術挑戰。
二、跨域的本質:瀏覽器的“安全守門人”
🧐 什么是同源策略?
同源策略(Same-Origin Policy) 是瀏覽器實施的核心安全策略,它要求網頁只能請求與其自身協議、域名、端口完全相同的資源。這就像現實生活中,你家的鑰匙只能打開你家的門,不能打開鄰居家的門一樣,是一種最基本的安全邊界。
🔍 同源判斷標準(三要素)
要素 | 說明 | 示例 |
---|---|---|
協議 | 通信協議必須相同 | http 與 https 不同 |
域名 | 主域名和子域名都必須相同 | www.example.com 與 api.example.com 不同 |
端口 | 網絡端口號必須相同 | 80 與 8080 不同 |
注意:IE 瀏覽器在判斷同源時存在例外,它不檢查端口,并且允許主域名相同的不同子域之間通信。這是歷史遺留問題,現代瀏覽器已修復此行為。
🚫 典型跨域場景示例
當前頁面 URL | 請求資源 URL | 是否跨域 | 原因 |
---|---|---|---|
http://www.example.com | https://www.example.com/api | ? 是 | 協議不同 (http vs https) |
http://www.example.com | http://www.baidu.com | ? 是 | 域名不同 |
http://www.example.com:80 | http://www.example.com:8080 | ? 是 | 端口不同 |
http://www.example.com | http://api.example.com | ? 是 | 子域名不同 |
http://www.example.com | http://www.example.com/path | ? 否 | 完全同源 |
💡 為什么需要同源策略?
同源策略看似“限制重重”,實則是保護用戶安全的重要屏障。它通過嚴格的邊界控制,構建了 Web 安全的第一道防線。沒有它,互聯網將變成危機四伏的“狂野西部”。
🔍 沒有同源策略的安全災難
想象一個沒有門禁系統的辦公樓——任何人都可以自由進出任何辦公室,翻閱文件柜,甚至冒充員工簽署文件。同源策略正是 Web 世界的門禁系統,防止以下三類致命攻擊:
1. Cookie 劫持攻擊:身份盜竊的溫床
攻擊原理:Cookie 通常存儲用戶登錄憑證。沒有同源限制,惡意網站可通過 document.cookie
直接讀取其他網站的 Cookie,獲取你的銀行賬戶、郵箱、社交平臺等登錄狀態。
真實案例:2018 年 Facebook 劍橋分析事件中,第三方應用通過獲取用戶 Cookie 數據,在未經許可情況下訪問了 8700 萬用戶的個人信息。
防護機制:同源策略禁止不同源頁面訪問 Cookie,配合 HttpOnly
屬性可進一步防止 JavaScript 讀取敏感 Cookie。
2. DOM 篡改攻擊:視覺欺詐的陷阱
攻擊原理:惡意網站可通過 JavaScript 操作其他網站的 DOM 結構,例如在銀行頁面上覆蓋虛假的登錄表單,或修改電商網站的支付金額。
典型場景:當你同時打開 yourbank.com
和 fakebank.com
時,后者可修改前者頁面內容,將轉賬金額從 100 元改為 10000 元,而你完全無法察覺。
防護機制:同源策略禁止跨域 DOM 訪問,確保每個網站的頁面內容只能被自身 JavaScript 操控。
3. 跨站請求偽造(CSRF):身份冒用的武器
攻擊原理:惡意網站可偽造請求,利用你已登錄的身份向其他網站發送操作指令。例如,當你登錄網銀后訪問惡意網站,它可自動發起轉賬請求。
技術實現:
<!-- 惡意網站隱藏表單 -->
<form action="https://yourbank.com/transfer" method="POST" id="stealForm"><input type="hidden" name="toAccount" value="attackerAccount" /><input type="hidden" name="amount" value="10000" />
</form>
<script>// 自動提交表單document.getElementById("stealForm").submit();
</script>
防護機制:同源策略限制跨域請求,結合 CSRF Token、Referer 驗證等機制可有效防范。
🌰 生動案例:一次未遂的銀行搶劫
假設你同時打開了兩個標簽頁:
https://yourbank.com
(已登錄網銀)https://malicious.com
(惡意網站)
沒有同源策略時:
- 惡意網站讀取你銀行頁面的 Cookie,獲取登錄狀態
- 修改銀行頁面 DOM,添加隱藏轉賬表單
- 自動提交表單,將你的資金轉移到攻擊者賬戶
同源策略如何防護:
? 阻止讀取銀行 Cookie
? 禁止修改銀行頁面 DOM
? 限制跨域請求發送
這就是為什么瀏覽器會嚴格執行同源策略——它不是技術限制,而是保護你數字財產的安全衛士。
三、跨域的表現:瀏覽器如何“攔截”請求?
很多開發者第一次遇到跨域問題時都會感到困惑:明明網絡請求成功了,服務器也返回了數據,為什么前端就是拿不到?要理解這個問題,我們需要深入了解瀏覽器攔截跨域請求的完整流程和技術細節。
- 網絡面板:顯示真實的請求和響應狀態(如 200 OK),因為這是服務器實際返回的狀態
- 控制臺:顯示 CORS 錯誤,因為瀏覽器攔截了響應,前端無法訪問數據
🔍 跨域錯誤的典型表現
當跨域請求被瀏覽器攔截時,控制臺會出現類似以下的錯誤信息(不同瀏覽器措辭略有差異):
常見錯誤類型及示例
- 缺少 CORS 頭部錯誤(最常見):
Access to fetch at 'http://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
- 憑據不允許錯誤:
Access to fetch at 'http://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
- 方法不允許錯誤:
Access to fetch at 'http://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.
🕵??♂? 關鍵真相:請求已發送,響應被攔截
重要理解:跨域請求實際已發送到服務器,服務器也已處理并返回響應,但瀏覽器在將響應交給前端 JavaScript 之前進行了攔截檢查。這個過程包含三個關鍵步驟:
瀏覽器攔截流程示意圖
瀏覽器攔截的三步流程
-
請求發送階段:
- 瀏覽器允許請求發送到目標服務器
- 自動添加
Origin
請求頭標識來源 - 對于非簡單請求,先發送預檢請求(OPTIONS)
-
服務器響應階段:
- 服務器處理請求并返回響應
- 若服務器未正確配置 CORS 頭部,響應中會缺少必要的允許信息
- 即使服務器返回 200 狀態碼,瀏覽器仍可能攔截響應
-
瀏覽器檢查階段:
- 瀏覽器檢查響應中的 CORS 頭部
- 若檢查不通過,丟棄響應數據并拋出控制臺錯誤
- 若檢查通過,將響應數據交給前端 JavaScript
這就是為什么你在網絡面板(Network)中能看到 200 狀態碼的響應,卻在控制臺看到 CORS 錯誤的原因。瀏覽器充當了“安全門衛”的角色,即使服務器已提供數據,也會基于安全策略決定是否將數據交給前端。
四、跨域解決方案全景:從基礎到高級
面對跨域問題,開發者們探索出了多種解決方案。選擇哪種方案取決于你的具體場景:
是開發環境還是生產環境?
是簡單的 GET 請求還是復雜的交互?
是否有權限修改服務器配置?
🅰? 方案一:CORS(跨域資源共享)—— 官方標準方案
CORS(Cross-Origin Resource Sharing) 通過服務器設置 HTTP 響應頭來告訴瀏覽器允許跨域請求,是 W3C 推薦的標準解決方案。
🔧 基本原理
- 瀏覽器發送請求時自動添加
Origin
頭,表明請求來源 - 服務器返回
Access-Control-Allow-Origin
等響應頭,表明是否允許該來源訪問 3.瀏覽器檢查響應頭,決定是否將數據交給前端
📝 核心響應頭配置
CORS 通過以下關鍵響應頭控制跨域訪問權限,每個頭部都有特定的用途和安全考量:
-
Access-Control-Allow-Origin
- 允許值:具體的源 URL(如
https://example.com
)或通配符*
- 作用:指定允許訪問資源的外部域
- 安全約束:生產環境中應明確指定源,避免使用
*
通配符;當請求需要攜帶憑據(如 Cookie)時,不能使用*
- 示例:
Access-Control-Allow-Origin: https://your-frontend.com
- 允許值:具體的源 URL(如
-
Access-Control-Allow-Methods
- 允許值:逗號分隔的 HTTP 方法列表(如
GET, POST, PUT, DELETE
) - 作用:指定允許的 HTTP 請求方法
- 安全約束:應僅開放必要的方法,遵循最小權限原則
- 示例:
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
- 允許值:逗號分隔的 HTTP 方法列表(如
-
Access-Control-Allow-Headers
- 允許值:逗號分隔的請求頭列表(如
Content-Type, Authorization
) - 作用:指定允許的自定義請求頭
- 注意事項:對于非簡單請求頭(如 Authorization),必須顯式聲明
- 示例:
Access-Control-Allow-Headers: Content-Type, Authorization
- 允許值:逗號分隔的請求頭列表(如
-
Access-Control-Allow-Credentials
- 允許值:布爾值
true
(僅當允許憑據時) - 作用:指示是否允許跨域請求攜帶憑據(如 Cookie、HTTP 認證信息)
- 安全考量:啟用此選項會增加安全風險,需確保源驗證嚴格
- 示例:
Access-Control-Allow-Credentials: true
- 允許值:布爾值
-
Access-Control-Max-Age
- 允許值:正整數(單位:秒)
- 作用:指定預檢請求(OPTIONS)結果的緩存時間
- 優化建議:合理設置緩存時間(如 86400 秒=24 小時)可減少預檢請求次數
- 示例:
Access-Control-Max-Age: 86400
這些響應頭需要配合使用,共同構成完整的 CORS 安全策略。服務器必須正確配置這些頭部才能使跨域請求正常工作。
💻 CORS 實現代碼示例
Node.js/Express 實現:
const express = require("express");
const app = express();// 全局CORS中間件
app.use((req, res, next) => {// 允許指定源訪問res.setHeader("Access-Control-Allow-Origin", "https://your-frontend.com");// 允許的方法res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");// 允許的請求頭res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");// 允許攜帶Cookieres.setHeader("Access-Control-Allow-Credentials", "true");// 處理預檢請求if (req.method === "OPTIONS") {res.statusCode = 204; // 預檢請求不需要響應體return res.end();}next();
});// API路由
app.get("/data", (req, res) => {res.json({ message: "跨域請求成功!" });
});app.listen(3000, () => {console.log("服務器運行在端口3000");
});
Nginx 配置:
server {listen ;server_name api.example.com;location / {# 允許跨域add_header Access-Control-$allow_origin https://example.com;add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE;add_header Access-Control-Allow-Headers Content-Type,Authorization;add_header Access-Control-Allow-Credentials true;# 預檢請求直接返回204if ($request_method = 'OPTIONS') {return ;}proxy_pass http://localhost:3000;}
}
?? CORS 安全最佳實踐
- 避免使用
*
通配符: 在生產環境中應明確指定允許訪問的源 - 限制允許的方法:只開放必要 HTTP 方法
- 謹慎啟用 Credentials:允許 Cookie 跨域傳輸會增加安全風險
- 合理設置 Max-Age:減少預檢請求次數提升性能
🅱? 方案二:JSONP —— 古老但仍在使用的技巧
JSONP (JSON with Padding) 是一種利用 <script>
標簽不受同源策略限制特性的跨域方案,雖然古老但在一些兼容性要求高的場景仍有應用。
🔧 工作原理
- 前端創建
<script>
標簽并指定服務器 URL,附帶回調函數名 - 服務器返回 JavaScript 代碼,格式為
回調函數名(數據)
- 瀏覽器執行返回的 JavaScript,調用回調函數處理數據
💻 JSONP 實現代碼示例
前端實現:
// 創建回調函數
function handleResponse(data) {console.log("JSONP 返回數據:", data);
}// 動態創建 script 標簽
function fetchDataWithJSONP() {const script = document.createElement("script");// 傳遞回調函數名給服務器script.src = "http://api.example.com/data?callback=handleResponse";document.body.appendChild(script);// 使用后移除 script 標簽script.onload = () => {document.body.removeChild(script);};
}// 調用函數發起請求
fetchDataWithJSONP();
服務器實現(Node.js):
const http = require('http');
const url = require('url');const server = http.createServer((req,const query = url.parse(req.url,const callback = query.callback;const data = JSON.stringify({ message: 'JSONP請求成功' });// 返回JavaScript代碼,調用回調函數res.writeHead(res.end(`${callback}(${data})`);
});server.listen(3000);
?? JSONP 的局限性
1.僅支持 GET 請求:無法發送 POST 等復雜請求
2.安全風險:可能遭受 XSS 攻擊
3.錯誤處理困難:缺乏標準的錯誤處理機制
4.無法設置請求頭:難以實現認證等功能
JSONP 已逐漸被 CORS 取代,但在需要兼容極低版本瀏覽器的場景仍有使用價值。
🅲? 方案三: 代理服務器 —— 前端無感方案
代理服務器通過在同域服務器端轉發請求來繞過瀏覽器同源限制,是開發環境中最常用方案之一。
🔧 工作原理
代理服務器充當中間人,將跨域請求轉發到目標服務器,前端只與同域代理服務器通信,瀏覽器不會觸發跨域限制。
-
前端將請求發送到同域代理服務器
-
代理服務器轉發請求到目標服務器
-
目標服務器返回響應給代理服務器
-
代理服務器將響應返回給前端
由于前端只與同域代理服務器通信瀏覽器不會觸發跨域限制
💻 開發環境代理配置
Vite 配置:
// vite.config.js
export default {server: {proxy: {"/api": {target: "http://api.example.com", //目標服務器changeOrigin: true, // 更改請求源rewrite: (path) => path.replace(/^\/api/, ""), // 可選重寫路徑},},},
};
Webpack 配置:
// webpack.config.js
module.exports = {devServer: {proxy:'/api': {target: 'http://api.example.com',changeOrigin: true,pathRewrite: {'^/api': ''}}}}
🚀 生產環境代理(Nginx)
server {listen ;server_name example.com;# 靜態資源location / {root /usr/share/nginx/html;index index.html;}# API代理location /api/ {proxy_pass http://api.example.com/; #轉發到目標服務器proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;}
}
?? 代理服務器的局限性
- 開發環境依賴:需要配置代理服務器,生產環境可能不適用
- 性能開銷:增加了請求轉發的延遲
- 安全風險:代理服務器可能成為攻擊目標,需加強安全配置
- 跨域限制:仍需服務器端配合,無法完全解決跨域問題
🅳? 方案四:WebSocket ——實時通信跨域方案
WebSocket 協議是 HTML5 引入的全雙工通信協議它不受同源策略限制,特別適合實時通信場景。
🔧 工作原理
WebSocket 通過一次握手建立持久連接之后的通信不再受同源策略限制。
💻 WebSocket 實現代碼
前端實現:
// 創建 WebSocket 連接
const socket = new WebSocket('ws://api.example.com/chat');// 連接建立時觸發
socket.addEventListener('open', (event) => {console.log('WebSocket 連接已建立');socket.send('Hello Server!'); // 發送消息
});// 接收服務器消息
socket.addEventListener('message', (event) => {console.log('收到消息:', event.data);
});// 連接關閉時觸發
socket.addEventListener('close', (event) => {console.log('WebSocket 連接已關閉');
});// 發生錯誤時觸發
socket.addEventListener('error', (event) => {console.error('WebSocket 錯誤:', event);
});
服務器實現(Node.js with ws 庫):
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });// 監聽連接
wss.on('connection', (ws) => {console.log('新客戶端連接');// 接收客戶端消息ws.on('message', (message) => {console.log('收到:', message.toString());ws.send('服務器已收到: ' + message.toString());});// 連接關閉ws.on('close', () => {console.log('客戶端已斷開');});
});
🅴? 其他跨域方案
方案 | 適用場景 | 原理 | 優缺點 |
---|---|---|---|
postMessage | 跨窗口/iframe 通信 | 窗口間通過 postMessage 方法傳遞數據 | 靈活但僅限窗口間通信 |
document.domain | 同主域不同子域 | 顯式設置 document.domain 為相同主域 | 簡單但僅限同主域場景 |
location.hash | iframe 通信 | 利用 URL 哈希值傳遞數據 | 兼容性好但數據量有限 |
window.name | iframe 通信 | 利用 window.name 屬性存儲數據 | 可存儲大量數據但實現復雜 |
五、深度解析:CORS 預檢請求
很多開發者在使用 CORS 時會遇到一個困惑為什么明明只發送了一個請求,瀏覽器網絡面板卻顯示兩個請求?這就是 CORS 的預檢請求機制在起作用。
🕵??♂? 什么是預檢請求?
預檢請求(Preflight Request) 是瀏覽器在發送某些跨域請求前,先發送一個 OPTIONS
方法請求到服務器,以確定服務器 是否允許實際請求。
🚦 觸發預檢請求的條件
當請求滿足以下任一條件時瀏覽器會自動發送預檢請求:
1. 使用非簡單方法
簡單方法包括:GET
、HEAD
、POST
非簡單方法包括:PUT
、DELETE
、CONNECT
、OPTIONS
、TRACE
、PATCH
2. 使用非簡單請求頭
簡單請求頭包括:
Accept
Accept-Language
Content-Language
Content-Type
(僅允許值為application/x-www-form-urlencoded
、multipart/form-data
或text/plain
)
非簡單請求頭示例:
Authorization
(認證令牌)Content-Type: application/json
(JSON 格式數據)X-Custom-Header
(自定義頭)
🔍 簡單請求完整示例
滿足以下條件的請求不會觸發預檢:
// 簡單GET請求示例
fetch("https://api.example.com/data", {method: "GET",headers: {Accept: "application/json","Accept-Language": "zh-CN",},
});
🔍 預檢請求完整示例
以下請求會觸發預檢:
// 帶自定義頭的POST請求(會觸發預檢)
fetch("https://api.example.com/data", {method: "POST",headers: {"Content-Type": "application/json", // 非簡單Content-TypeAuthorization: "Bearer token123", // 非簡單請求頭"X-User-ID": "12345", // 自定義頭},body: JSON.stringify({ name: "跨域請求" }),
});
預檢請求/響應流程:
- 預檢請求(OPTIONS):瀏覽器自動發送
OPTIONS /data HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type,Authorization,X-User-ID
- 預檢響應:服務器返回
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type,Authorization,X-User-ID
Access-Control-Max-Age: 86400
- 實際請求:預檢通過后發送真實請求
?? 預檢請求優化
頻繁的預檢請求會影響性能,可通過以下方式優化:
- 設置合理的 Max-Age:緩存預檢結果(單位:秒)
- 避免使用自定義頭:優先使用簡單請求頭
- 合并請求:減少跨域請求次數
- 使用 GET 替代 POST:GET 請求通常為簡單請求
六、總結
CORS 預檢請求機制是為了確保跨域請求的安全性而引入的。開發者在使用 CORS 時需要注意觸發預檢請求的條件,以及合理配置服務器端響應頭。通過合理優化預檢請求,能夠提升應用的性能和用戶體驗。
希望本文能夠幫助你理解跨域的本質、同源策略的作用,以及如何通過 CORS、JSONP、代理等多種方式解決跨域問題。跨域雖然是 Web 開發中的一個挑戰,但也是提升應用安全性和用戶體驗的重要環節,好好利用可以讓你的應用程序更加高效。😉