JavaScript
單元:Fetch 與網路請求(Networking)
主題:CORS 原理
簡介
在前端開發中,跨來源資源共享(Cross‑Origin Resource Sharing,簡稱 CORS) 是每一位使用 fetch、XMLHttpRequest 或其他 AJAX 技術的開發者必須面對的概念。
瀏覽器為了保護使用者的資料安全,預設只允許同源(same‑origin)的請求;當前端程式需要向不同網域的 API 取得資料時,就會觸發 CORS 機制。
了解 CORS 的運作原理不僅可以避免「被瀏覽器阻擋」的尷尬錯誤,還能協助你在設計前後端介面時,正確設定 HTTP 標頭、避免安全漏洞,提升應用的可用性與可靠性。
核心概念
1. 同源政策(Same‑Origin Policy)
同源政策是瀏覽器的基礎安全模型,判斷「同源」的條件包括 協議(protocol)、主機(host) 與 埠號(port)。
例如:
| URL | 協議 | 主機 | 埠號 |
|---|---|---|---|
https://example.com/api |
https | example.com | 443 |
http://example.com/api |
http | example.com | 80 |
https://api.example.com |
https | api.example.com | 443 |
上述三個 URL 中,只有第一個與 https://example.com 完全相同,才算是同源。
2. 為什麼會有 CORS
當前端要向 不同來源(例如前端在 https://myapp.com,而 API 在 https://api.partner.com)發送請求時,瀏覽器會先檢查回應的 CORS 標頭,決定是否允許這筆跨域請求。若標頭缺失或不符合規範,瀏覽器會直接拋出 CORS 錯誤,而不會把回應內容交給 JavaScript。
3. CORS 的三個主要標頭
| 標頭 | 作用 | 範例 |
|---|---|---|
Access-Control-Allow-Origin |
指定允許的來源(* 代表所有) |
Access-Control-Allow-Origin: https://myapp.com |
Access-Control-Allow-Methods |
允許的 HTTP 方法(GET、POST、PUT…) | Access-Control-Allow-Methods: GET, POST, OPTIONS |
Access-Control-Allow-Headers |
允許的自訂請求標頭 | Access-Control-Allow-Headers: Content-Type, Authorization |
此外,Access-Control-Allow-Credentials 用於告訴瀏覽器是否允許傳送 Cookie、HTTP 認證資訊。若設定為 true,則 Access-Control-Allow-Origin 不能 為 *,必須明確列出來源。
4. 預檢請求(Preflight Request)
對於 非簡單請求(simple request)——例如使用自訂標頭、使用 PUT、DELETE、或 Content-Type 為 application/json 等情況,瀏覽器會先發送一個 OPTIONS 方法的 預檢請求,檢查伺服器是否接受這類請求。
預檢流程簡述:
- 瀏覽器送
OPTIONS /resource,帶上Origin、Access-Control-Request-Method、Access-Control-Request-Headers。 - 伺服器回應
200,並附上Access-Control-Allow-*系列標頭。 - 若回應符合要求,瀏覽器才會發送真正的請求(如
POST、PUT)。
5. 簡單請求(Simple Request)
符合以下條件的請求不會觸發預檢:
| 條件 | 說明 |
|---|---|
方法 必須是 GET、HEAD、POST |
|
標頭 只能是 Accept、Accept-Language、Content-Language、Content-Type(且 Content-Type 只能是 application/x-www-form-urlencoded、multipart/form-data、text/plain) |
|
無自訂標頭(如 X-My-Header) |
程式碼範例
下面提供 5 個實用範例,從最簡單的同源請求到完整的 CORS 設定,讓你一步步掌握概念。
範例 1:同源的 fetch(不涉及 CORS)
// 這是一個同源請求,瀏覽器直接允許
fetch('/api/user')
.then(res => res.json())
.then(data => console.log('User:', data))
.catch(err => console.error('Error:', err));
說明:因為請求的 URL 與目前網頁同源,瀏覽器不會檢查 CORS 標頭。
範例 2:跨域的簡單 GET(會檢查 Access-Control-Allow-Origin)
// 目標 API 位於 https://api.example.com
fetch('https://api.example.com/public-data')
.then(res => {
if (!res.ok) throw new Error('Network response was not ok');
// 若 CORS 標頭不符合,這裡會直接拋出錯誤
return res.json();
})
.then(data => console.log('Public data:', data))
.catch(err => console.error('CORS error:', err));
伺服器端(Node.js Express 範例):
app.get('/public-data', (req, res) => {
res.set('Access-Control-Allow-Origin', '*'); // 允許所有來源
res.json({ message: 'Hello from API' });
});
範例 3:跨域的 POST,觸發預檢(Preflight)
fetch('https://api.example.com/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // 非簡單的 Content-Type
'X-Requested-With': 'XMLHttpRequest' // 自訂標頭
},
body: JSON.stringify({ name: 'Alice' }),
credentials: 'include' // 想要帶上 Cookie
})
.then(res => res.json())
.then(result => console.log('Result:', result))
.catch(err => console.error('CORS error:', err));
伺服器端(Express):
app.options('/submit', (req, res) => {
// 回應預檢
res.set({
'Access-Control-Allow-Origin': 'https://myapp.com',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '86400' // 預檢結果快取一天
});
res.sendStatus(204);
});
app.post('/submit', (req, res) => {
// 真正的處理邏輯
res.set({
'Access-Control-Allow-Origin': 'https://myapp.com',
'Access-Control-Allow-Credentials': 'true'
});
res.json({ success: true, data: req.body });
});
範例 4:使用 mode: 'cors' 與 credentials 的細節
fetch('https://api.example.com/user-info', {
method: 'GET',
mode: 'cors', // 明確告訴瀏覽器這是跨域請求
credentials: 'include' // 允許攜帶 Cookie、HTTP 認證
})
.then(res => res.json())
.then(info => console.log('User info:', info))
.catch(err => console.error('CORS error:', err));
重點:
mode: 'cors'為預設值,但在某些情況(如no-cors)會限制可讀取的回應。- 若伺服器未回傳
Access-Control-Allow-Credentials: true,include會被瀏覽器自動忽略。
範例 5:在前端偵測 CORS 錯誤並顯示友善訊息
async function getData() {
try {
const res = await fetch('https://api.partner.com/data');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
console.log(json);
} catch (e) {
if (e instanceof TypeError && e.message.includes('Failed to fetch')) {
// 大多是 CORS 被阻擋
alert('無法取得資料,可能是 CORS 設定有誤,請聯絡系統管理員。');
} else {
console.error(e);
}
}
}
說明:
TypeError: Failed to fetch常見於 CORS 被拒絕,透過此方式可以在 UI 上給予使用者明確的提示。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 最佳實踐 |
|---|---|---|
使用 * 搭配 credentials: 'include' |
瀏覽器會直接拋出錯誤,因為 * 代表「任何來源」且無法傳送憑證。 |
若需要 Cookie,必須在 Access-Control-Allow-Origin 中寫明確的來源,且同時設定 Access-Control-Allow-Credentials: true。 |
忘記回傳 Access-Control-Allow-Headers |
自訂標頭未被允許,導致預檢失敗。 | 在伺服器端列出所有可能的自訂標頭,或使用 Access-Control-Allow-Headers: *(部分瀏覽器支援)。 |
| 預檢過於頻繁 | 每次跨域 POST 都會觸發 OPTIONS,增加延遲。 |
使用 Access-Control-Max-Age 設定快取時間(例如 86400 秒),降低預檢請求次數。 |
| 忽略 HTTPS | 若前端是 HTTPS,跨域的 HTTP 端點會被瀏覽器視為不安全而阻擋。 | 確保 所有端點皆使用相同的協議,或在開發環境使用本機代理解決。 |
在前端硬編碼 mode: 'no-cors' |
會讓回應變成 opaque(不可讀),常見於想「繞過」CORS 卻失去資料。 | 只在真的不需要讀取回應內容(如圖片、腳本)時使用,否則應正確設定伺服器 CORS。 |
實際應用場景
SPA 與微服務
現代單頁應用(React、Vue、Angular)常把 UI 與後端 API 分別部署在不同子域或不同雲端服務上。此時必須在每個微服務的入口(Gateway)加上正確的 CORS 設定,才能讓前端自由呼叫。第三方 API 整合
例如使用 Google Maps、Stripe、GitHub API 等外部服務。大部分服務已預先在伺服器端開啟Access-Control-Allow-Origin: *,但若需要傳送認證資訊(Bearer Token),仍需確認是否支援Access-Control-Allow-Credentials。跨域上傳檔案
使用FormData透過POST上傳檔案時,Content-Type會自動變為multipart/form-data,屬於簡單請求。但若同時加上自訂標頭(例如X-CSRF-Token),就會觸發預檢,開發者必須在伺服器端處理OPTIONS。企業內部系統
多個子系統(ERP、CRM、BI)可能在不同內網域上運行,透過 CORS 可以在不改動 DNS 或代理的情況下,讓前端一次性呼叫多個系統的 API。
總結
- CORS 是瀏覽器為保護使用者而設的跨域安全機制,核心依賴 HTTP 標頭(
Access-Control-Allow-*)來決定是否允許請求。 - 簡單請求 不會觸發預檢;非簡單請求(自訂標頭、非 GET/HEAD/POST、特殊 Content-Type)會先發送
OPTIONS預檢。 Access-Control-Allow-Origin必須正確對應來源;若要傳送憑證,Access-Control-Allow-Credentials必須為true,且來源不能是*。- 常見陷阱包括
*與credentials同時使用、忘記回傳允許的自訂標頭、預檢過於頻繁等。透過 快取 (Access-Control-Max-Age)、明確列出來源、使用 HTTPS 等最佳實踐,可大幅提升開發效率與安全性。
掌握 CORS 後,你就能在 Fetch、Axios、XMLHttpRequest 等工具中自信地與任何跨域 API 互動,為前端應用打造更穩定、可擴充的網路請求層。祝你開發順利!