在前端開發中,"跨域"是一個繞不開的話題。當我們的頁面嘗試從一個域名請求另一個域名的資源時,瀏覽器往往會拋出類似Access to fetch at 'xxx' from origin 'xxx' has been blocked by CORS policy
的錯誤。下面將深入探討跨域請求的底層原理,并介紹多種解決跨域問題和解決方案。
一、跨域的本質:同源策略
要理解跨域,首先需要了解瀏覽器的同源策略(Same-Origin Policy)。這是瀏覽器最核心的安全功能之一,由Netscape在1995年引入,其目的是防止惡意網頁竊取另一個網頁的敏感數據。
1.1 什么是"同源"?
兩個URL被視為"同源",必須同時滿足以下三個條件:
- 協議相同(如都是http或https)
- 域名相同(如都是example.com,而非a.example.com和b.example.com)
- 端口相同(如都是80端口,默認端口可省略)
舉例說明:
當前頁面URL | 請求目標URL | 是否同源 | 原因 |
---|---|---|---|
http://example.com | http://example.com/page | 是 | 三要素完全相同 |
http://example.com | https://example.com | 否 | 協議不同(http vs https) |
http://example.com | http://api.example.com | 否 | 域名不同(主域 vs 子域) |
http://example.com:80 | http://example.com:8080 | 否 | 端口不同(80 vs 8080) |
1.2 同源策略的限制范圍
同源策略主要限制以下幾種交互:
- DOM訪問:禁止不同源頁面之間的DOM操作(如iframe嵌套的跨域頁面)
- 數據讀取:禁止讀取不同源的Cookie、LocalStorage、SessionStorage
- 網絡請求:禁止通過XMLHttpRequest、Fetch API等方式發起跨域HTTP請求
注意:并非所有跨域請求都會被禁止。像<img>
、<script>
、<link>
等標簽的資源加載不受同源策略限制,這也是后續某些跨域解決方案的技術基礎。
二、跨域請求的類型:簡單請求與預檢請求
當瀏覽器檢測到跨域請求時,會根據請求的特征將其分為兩類,并采取不同的處理策略:
2.1 簡單請求(Simple Request)
同時滿足以下條件的請求被視為簡單請求:
- 請求方法為以下三種之一:
GET
、HEAD
、POST
- 請求頭僅包含瀏覽器默認字段或以下字段:
Accept
、Accept-Language
、Content-Language
、Content-Type
(僅限特定值) Content-Type
的值只能是:application/x-www-form-urlencoded
、multipart/form-data
、text/plain
簡單請求的處理流程:
- 瀏覽器直接發送請求,并在請求頭中添加
Origin
字段(值為當前頁面域名) - 服務器響應時,若包含
Access-Control-Allow-Origin
且值包含請求的Origin
,則瀏覽器允許前端讀取響應;否則攔截響應,拋出跨域錯誤
2.2 預檢請求(Preflight Request)
不滿足簡單請求條件的跨域請求會觸發預檢請求,例如:
- 使用
PUT
、DELETE
等特殊請求方法 - 請求頭包含自定義字段(如
Authorization
、X-Requested-With
) Content-Type
為application/json
預檢請求的處理流程:
- 瀏覽器先發送一個
OPTIONS
方法的預檢請求,詢問服務器是否允許實際請求 - 服務器響應預檢請求時,通過
Access-Control-*
系列頭字段聲明允許的跨域規則 - 若服務器允許,瀏覽器才發送實際請求;否則直接攔截,不發送實際請求
三、跨域解決方案及實踐
了解跨域的原理后,我們來介紹幾種常用的跨域解決方案,每種方案都將提供完整的代碼示例。
3.1 CORS(Cross-Origin Resource Sharing)
CORS是W3C標準推薦的跨域解決方案,通過服務器端設置響應頭實現跨域允許,支持所有HTTP方法,是目前最主流的跨域方案。
3.1.1 基本原理
CORS的核心是服務器端通過設置Access-Control-*
系列響應頭,告知瀏覽器允許哪些跨域請求。常用頭字段包括:
Access-Control-Allow-Origin
:允許的源(如https://example.com
或*
表示允許所有)Access-Control-Allow-Methods
:允許的請求方法(如GET, POST, PUT
)Access-Control-Allow-Headers
:允許的請求頭Access-Control-Allow-Credentials
:是否允許攜帶憑證(Cookie等)Access-Control-Max-Age
:預檢請求的緩存時間(避免重復發送預檢請求)
3.1.2 代碼示例
前端代碼(使用Fetch API):
// 前端頁面地址:http://localhost:3000
fetch('http://localhost:4000/api/data', {method: 'POST',headers: {'Content-Type': 'application/json','X-Custom-Header': 'custom-value' // 自定義頭,會觸發預檢請求},body: JSON.stringify({ name: '前端請求' }),credentials: 'include' // 允許攜帶Cookie
})
.then(response => response.json())
.then(data => console.log('跨域請求成功:', data))
.catch(error => console.error('跨域請求失敗:', error));
后端代碼(Node.js + Express):
// 服務器地址:http://localhost:4000
const express = require('express');
const app = express();
app.use(express.json());// CORS配置中間件
app.use((req, res, next) => {// 允許的源(生產環境建議指定具體域名,而非*)res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');// 允許的請求方法res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');// 允許的請求頭(需包含前端實際使用的所有自定義頭)res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header');// 允許攜帶憑證(Cookie等)res.setHeader('Access-Control-Allow-Credentials', 'true');// 預檢請求緩存時間(秒)res.setHeader('Access-Control-Max-Age', '86400'); // 24小時// 處理預檢請求(直接返回204)if (req.method === 'OPTIONS') {return res.sendStatus(204);}next();
});// 接口路由
app.post('/api/data', (req, res) => {console.log('收到跨域請求數據:', req.body);res.json({ status: 'success', message: '跨域請求處理完成',data: req.body});
});app.listen(4000, () => {console.log('CORS服務器運行在 http://localhost:4000');
});
注意:當Access-Control-Allow-Credentials
設為true
時,Access-Control-Allow-Origin
不能設為*
,必須指定具體域名。
3.2 JSONP(JSON with Padding)
JSONP是一種古老但兼容性極佳的跨域方案(支持IE等老瀏覽器),其原理是利用<script>
標簽不受同源策略限制的特性,通過動態創建<script>
標簽發起跨域請求。
3.2.1 基本原理
- 前端定義一個回調函數(如
handleJsonpResponse
) - 前端動態創建
<script>
標簽,其src
指向跨域接口,并在URL中攜帶回調函數名(如?callback=handleJsonpResponse
) - 服務器接收到請求后,將數據包裹在回調函數中返回(如
handleJsonpResponse({...})
) - 瀏覽器加載
<script>
后,自動執行回調函數,前端即可獲取數據
3.2.2 代碼示例
前端代碼:
<!-- 前端頁面地址:http://localhost:3000 -->
<script>
// 定義回調函數
function handleJsonpResponse(data) {console.log('JSONP跨域請求成功:', data);
}// 動態創建script標簽發起請求
function requestJsonp() {const script = document.createElement('script');// 跨域接口地址,攜帶回調函數名script.src = 'http://localhost:4000/api/jsonp?callback=handleJsonpResponse&name=jsonp請求';document.body.appendChild(script);// 請求完成后移除script標簽script.onload = () => script.remove();script.onerror = () => {console.error('JSONP請求失敗');script.remove();};
}
</script><button onclick="requestJsonp()">發起JSONP請求</button>
后端代碼(Node.js + Express):
// 服務器地址:http://localhost:4000
const express = require('express');
const app = express();app.get('/api/jsonp', (req, res) => {const { callback, name } = req.query;console.log('收到JSONP請求參數:', name);// 構造響應數據(用回調函數包裹)const data = {status: 'success',message: 'JSONP請求處理完成',data: { name }};// 返回JavaScript代碼(執行回調函數)res.send(`${callback}(${JSON.stringify(data)})`);
});app.listen(4000, () => {console.log('JSONP服務器運行在 http://localhost:4000');
});
局限性:
- 僅支持
GET
請求 - 安全性風險(可能遭受XSS攻擊)
- 無法捕獲HTTP錯誤狀態碼(如404、500)
3.3 代理服務器
代理服務器是開發環境中常用的跨域解決方案,其原理是:由于瀏覽器的同源策略只限制前端腳本,不限制服務器之間的通信,因此可以通過一個與前端同源的代理服務器轉發請求到目標服務器。
3.3.1 開發環境代理(以Vite為例)
在前端項目中(如Vue、React),可通過開發服務器配置代理,解決開發階段的跨域問題。
Vite配置示例(vite.config.js):
// 前端開發服務器:http://localhost:5173
export default {server: {// 配置代理proxy: {// 匹配所有以/api開頭的請求'/api': {target: 'http://localhost:4000', // 目標服務器地址changeOrigin: true, // 發送請求時,將Host頭改為目標服務器地址// 可選:重寫路徑(如果目標接口沒有/api前綴)// rewrite: (path) => path.replace(/^\/api/, '')}}}
};
前端請求代碼:
// 此時請求的是同源的開發服務器(http://localhost:5173),無跨域問題
// 開發服務器會自動轉發到 http://localhost:4000/api/data
fetch('/api/data', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ name: '通過代理請求' })
})
.then(response => response.json())
.then(data => console.log('代理請求成功:', data));
3.3.2 生產環境代理(Nginx)
生產環境中,可通過Nginx反向代理實現跨域,配置示例如下:
# Nginx配置
server {listen 80;server_name localhost;# 前端頁面所在目錄location / {root /path/to/frontend;index index.html;}# 代理跨域請求location /api/ {# 目標服務器地址proxy_pass http://localhost:4000/api/;# 傳遞原始請求頭proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;# 可選:設置CORS頭(如果目標服務器未設置)add_header Access-Control-Allow-Origin *;}
}
配置后,前端直接請求/api/data
,Nginx會自動轉發到http://localhost:4000/api/data
,避免跨域問題。
3.4 其他跨域方案
3.4.1 iframe + postMessage
適用于兩個跨域頁面之間的通信(如父頁面與iframe嵌套頁面):
父頁面(http://parent.com):
<iframe id="childFrame" src="http://child.com"></iframe><script>
// 向子頁面發送消息
const frame = document.getElementById('childFrame');
frame.onload = () => {frame.contentWindow.postMessage({ type: 'greeting', data: 'Hello from parent' },'http://child.com' // 限制接收域);
};// 接收子頁面消息
window.addEventListener('message', (event) => {// 驗證消息來源if (event.origin !== 'http://child.com') return;console.log('收到子頁面消息:', event.data);
});
</script>
子頁面(http://child.com):
// 接收父頁面消息
window.addEventListener('message', (event) => {if (event.origin !== 'http://parent.com') return;console.log('收到父頁面消息:', event.data);// 向父頁面回復消息event.source.postMessage({ type: 'response', data: 'Hello from child' },event.origin);
});
3.4.2 WebSocket
WebSocket協議本身不受同源策略限制,可直接建立跨域連接:
前端代碼:
// 建立WebSocket連接(ws/wss協議)
const socket = new WebSocket('ws://localhost:4000');// 連接成功
socket.onopen = () => {console.log('WebSocket連接已建立');socket.send('Hello WebSocket');
};// 接收消息
socket.onmessage = (event) => {console.log('收到WebSocket消息:', event.data);
};
后端代碼(Node.js + ws庫):
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 4000 });wss.on('connection', (ws) => {console.log('客戶端已連接');ws.on('message', (message) => {console.log('收到消息:', message.toString());ws.send('服務器收到:' + message.toString());});
});
四、總結與最佳實踐
跨域請求的解決方案各有優缺點,選擇時需根據實際場景判斷:
方案 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
CORS | 功能完善、支持所有HTTP方法、安全性高 | 需要服務器配合、老瀏覽器兼容問題 | 現代Web應用(推薦) |
JSONP | 兼容性好(支持IE) | 僅支持GET、安全性差 | 需兼容老瀏覽器的場景 |
代理服務器 | 前端無需修改、開發/生產均可用 | 需要額外配置服務器 | 開發環境調試、生產環境跨域 |
iframe + postMessage | 適合頁面間通信 | 僅用于頁面交互、不適合API請求 | 父頁面與iframe跨域通信 |
WebSocket | 全雙工通信、無跨域限制 | 需專門協議、不適合普通API請求 | 實時通信場景(如聊天、通知) |
最佳實踐建議:
- 優先使用CORS,這是最標準、最安全的跨域方案
- 開發環境使用代理服務器(如Vite、Webpack代理)提高開發效率
- 生產環境避免使用
*
作為Access-Control-Allow-Origin
,嚴格限制允許的源 - 涉及用戶憑證的請求,確保正確配置
Access-Control-Allow-Credentials
- 避免使用JSONP,除非有強烈的老瀏覽器兼容需求
通過本文的介紹,相信你已經對跨域請求的原理和解決方案有了全面的理解。在實際開發中,結合具體場景選擇合適的方案,就能輕松解決跨域問題。