在前端與後端分離的時代,網頁開發者幾乎都曾面臨過跨來源請求與跨域資源共用(CORS, Cross-Origin Resource Sharing)所導致的錯誤與挑戰。CORS 提供了一套嚴謹的機制,透過瀏覽器的安全策略來協助我們在不同網域間傳遞、取得資源,以避免潛在的安全風險。本篇文章將透過詳細的範例與解說,帶領讀者深入了解 CORS 的運作原理、核心概念與常見解法,並彙整應用時可能會遇到的問題與最佳實踐方法。
以下內容將會從瀏覽器的「同源政策(Same-Origin Policy)」談起,然後進入到 CORS 的基礎原理、請求種類(簡單請求與非簡單請求)、預檢請求(preflight)、各種 HTTP 回應標頭的意義、實際應用的方式(如在 Express 或 Vite 中如何設定),最後輔以常見問題與注意事項做完整的說明。
什麼是同源政策(Same-Origin Policy)
瀏覽器的同源政策(Same-Origin Policy)是基於安全考量所提出的一種限制機制,用於防止網頁惡意地存取或操作非同源網域的資源。「同源」的判斷條件有三個要素必須全部相同:
- 通訊協定(Protocol):如 http:// 或 https://
- 網域(域名 Domain 或 Host):如 www.example.com
- 通訊埠(Port):如 :80、:443、:3000 等
只有滿足以上三者都一致,才被視為同源。因此,一個網頁(例如 https://www.example.com)只能完全地存取同源的資源或網站。如果換成 http://www.example.com(不同協定)或 https://api.example.com(不同主機),或 https://www.example.com:8080(不同通訊埠),都會造成跨源存取。
同源政策的初衷是避免惡意網站利用你當前的瀏覽器上下文(例如已登入的 session 或 Cookie),去偷偷對銀行網站或其他服務進行敏感操作,造成安全性與隱私上的重大風險。
CORS 與同源政策的關係
有了同源政策後,瀏覽器對跨來源的存取有明顯的安全限制。但在現代網站的架構中,經常需要client端和server端之間從不同網域或後端伺服器拉取資料,比如常見的前後端分離模式,或是請求第三方服務等等。
跨來源資源共用(CORS)便是為了在安全性與便利性之間達到平衡而誕生的機制。CORS 透過在http標頭增加一系列控制與授權標頭,讓瀏覽器得知「此服務器明確允許特定或全部來源存取我的資源」,進而在瀏覽器端放行該跨域的回應。
CORS 可以看作是對同源政策的「擴充標準」。它並沒有廢除同源政策本身,反而是提供一種在後端同意的情況下,讓合法、受信任的跨域請求得以被瀏覽器接收的方式。
瀏覽器如何判斷跨域:Origin 的定義
對於瀏覽器而言,每一次的 AJAX 或 Fetch API 請求都會帶有一個 Origin 字段,指出該請求是從哪個「來源請求(from origin)」發起。所謂「來源」http 就是由「協定 + 網域 + 通訊埠」三者組成。範例如下:
- http://localhost:3000 的來源是 http://localhost:3000
- https://www.example.com 的來源是 https://www.example.com
- http://www.example.com:8080 的來源是 http://www.example.com:8080
這些資訊會在跨域(非同源)時特別重要,因為後端伺服器需要根據請求帶來的 Origin 決定是否回應 CORS 相關標頭來授權,如 control allow origin https 來授權。
CORS 主要概念:簡單請求與非簡單請求
在瀏覽器端,對跨來源請求的 HTTP 作了兩類區分:「簡單請求(Simple Request)」與「非簡單請求(Not-so-simple Request)」。它們在底層的 CORS 流程有所不同,最大區別在於非簡單請求需要先進行「預檢請求(preflight)」。
簡單請求(Simple Request)
只要符合下述所有條件,該跨域請求就被視為「簡單」:
- 使用的 HTTP 方法僅限於 GET、POST、HEAD 其中之一。
- 自訂的 Request Header 僅限瀏覽器默認能帶的幾種標頭(不含額外自定義標頭)。
- Content-Type 只能是以下三種:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
換句話說,若你發送的是 GET 或 POST 並使用表單格式(application/x-www-form-urlencoded,或傳遞一般文本),沒有其他自定義標頭(像 X-Custom-Header)的話,多半就會被歸類為簡單請求。
非簡單請求(Not-so-simple Request)
凡是不符合簡單請求上述條件的請求,都被歸類為非簡單請求,例如:
- HTTP 方法是 PUT、DELETE、OPTIONS 或其他非 GET、POST、HEAD。
- 自定義標頭,如 X-Custom-Header、Authorization、Content-Type: application/json 等。
- Content-Type 屬於 application/json、text/xml……等超出簡單範圍的格式。
非簡單請求之所以特別,被認為可能對伺服器資源具有「副作用」,可能刪除、修改或新增資料,因此需要「預檢」來確保伺服器同意。
CORS 欄位與流程:預檢請求(Preflight Request)
簡單請求流程
假如你的請求屬於簡單請求,瀏覽器在發送該跨域請求時,會自動加入一個 Origin 標頭,告訴伺服器「我的來源是什麼」。接著,伺服器若允許跨源,就會在回應中加入:
Access-Control-Allow-Origin: <對應的來源或 *>
若這個值與發送的 Origin 不符合,瀏覽器就會把回應擋住,不讓前端讀取;若符合,才放行並讓前端讀取回應。
範例
Request:
GET /data
Host: api.example.com
Origin: http://localhost:3000
...
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Content-Type: application/json
...
{
"message": "Hello from server"
}
前端收到這個回應,由於 Access-Control-Allow-Origin 剛好等於 http://localhost:3000,瀏覽器放行並返回資料給 JS。
非簡單請求流程
預檢請求(Preflight)
當請求為非簡單請求(例如 PUT, DELETE, 帶有自定義標頭或 JSON body 等),瀏覽器在正式發送真正的請求前,會先發送一個 HTTP OPTIONS 請求(稱為預檢請求)。這個 OPTIONS 請求會帶有以下重要標頭:
- Origin: 表示來源
- Access-Control-Request-Method: 表示實際請求將使用什麼 HTTP 方法,例如 PUT
- Access-Control-Request-Headers: 列出實際請求將包含的自定義標頭,例如 X-Custom-Header, Content-Type
Server 收到預檢請求後,若願意允許該跨域操作,需要回應:
- Access-Control-Allow-Origin: 允許的來源
- Access-Control-Allow-Methods: 允許使用的 HTTP 方法列表(例如 GET, POST, PUT, DELETE)
- (可選)Access-Control-Allow-Headers: 若有自定義標頭,這裡必須明確列出;否則瀏覽器後續真正請求會被擋
- (可選)Access-Control-Max-Age: 多久時間內可以快取這個預檢回應,避免重複發送預檢
若伺服器允許,則瀏覽器才會發送真正的跨域請求;反之則會擋下。這個額外的「預檢」過程就是非簡單請求多出來的一道手續。
實際請求
預檢通過後,瀏覽器才會正式發送原本預計的 PUT / DELETE / 其他方法的跨域請求。此時,同樣會帶 Origin 標頭,伺服器也需要回應:
Access-Control-Allow-Origin: <符合原請求的 origin>
瀏覽器驗證後方能將回應交付給前端程式。
預檢請求與瀏覽器行為
- 快取機制
瀏覽器可以根據伺服器回傳的 Access-Control-Max-Age 欄位來暫存預檢結果。若在該段時間內再次遇到同樣來源、同樣方法、同樣標頭的非簡單請求,就能直接跳過預檢,減少網路負擔。 - 檢查重定向
有些瀏覽器對預檢後的重定向行為比較嚴苛,若伺服器在預檢後又進行跨源重定向,可能觸發錯誤,瀏覽器會拒絕或視為 CORS 失敗。因此,需要謹慎設定。
CORS 常見 HTTP Header 詳解
Request 端
Origin
- 用途:表明發出此請求的網頁來源(協定 + 網域 + Port)。
- 範例:Origin: https://www.example.com
- 瀏覽器在執行跨域請求(簡單或非簡單)時自動加上,前端程式開發者無法手動修改。
Access-Control-Request-Method
- 用途:非簡單請求的預檢請求才會帶上,表示後續將要使用的 HTTP 方法,如 PUT, DELETE 等。
- 範例:Access-Control-Request-Method: PUT
Access-Control-Request-Headers
- 用途:非簡單請求的預檢中帶上,列出後續將使用的所有自定義標頭。
- 範例:Access-Control-Request-Headers: Content-Type, X-PINGOTHER
Response 端
Access-Control-Allow-Origin
- 用途:伺服器回應時告知瀏覽器,「允許哪些來源」可以讀取此回應。可以是指定網域,也可以是 * 表示全部允許。
- 範例:
- Access-Control-Allow-Origin: https://www.example.com
- Access-Control-Allow-Origin: *
- 注意:若要包含 Cookie(Credentials),此欄位不能是 *,必須指定確切的網域。
Access-Control-Allow-Methods
- 用途:回應預檢請求時,表示伺服器允許的 HTTP 方法列表。
- 範例:Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers
- 用途:回應預檢請求時,表示允許使用的自訂標頭(含常見如 Content-Type, Authorization 等)。
- 範例:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type, Authorization
Access-Control-Allow-Credentials
- 用途:表示是否允許瀏覽器發送或接收跨源請求時攜帶憑證(Cookie、HTTP Authentication)。若值為 true,表示允許;若無此欄位則表示不允許攜帶。
- 範例:Access-Control-Allow-Credentials: true
Access-Control-Max-Age
- 用途:告知瀏覽器可以將「預檢」的結果快取多久(秒)。在這段期間相同條件的請求可以直接跳過預檢。
- 範例:Access-Control-Max-Age: 86400(一天)
Access-Control-Expose-Headers
- 用途:預設情況下,瀏覽器在 CORS 請求的回應中,只能取到少數幾個簡單標頭。若後端希望前端能讀取更多自定義標頭或非預設標頭,需在此列出。
- 範例:Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
CORS 中的 Cookie 與憑證處理(Credentials)
預設情況下,瀏覽器的跨域請求是不會附帶任何的 Cookie 或是認證資訊(如 HTTP Basic Auth)的。這是為了降低 CSRF 的安全風險。但在某些場景下,我們需要允許瀏覽器攜帶用戶的 Session Token、Cookie;或讓伺服器在跨域情況下設置 Cookie。
必須同時滿足的條件
- 前端
- 若使用 fetch,需要在初始化時指定 fetch(url, { credentials: ‘include’ })。
- 若使用 XMLHttpRequest,則要設 xhr.withCredentials = true。
- 後端
- 回應標頭中要有 Access-Control-Allow-Credentials: true。
- 同時,Access-Control-Allow-Origin 不得是 *,而必須明確指定某個網域(如 http://localhost:3000)。
符合以上條件後,瀏覽器才會在跨域請求中附帶或設置 Cookie。此外,若有第三方 Cookie 規則或 SameSite 屬性,仍需配合相應策略設定。
常見解法與實作方式
後端允許跨域:設定 Access-Control-Allow-Origin
最直接且「正規」的做法,就是在後端伺服器針對跨域來源設定 CORS 回應標頭。
範例(Node.js + Express):
// Node.js + Express 範例
app.get('/api/data', (req, res) => {
// 允許指定網域
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
// 若需要攜帶憑證的情況
// res.setHeader('Access-Control-Allow-Credentials', 'true');
res.json({ message: 'Hello from server' });
});
或是使用萬用字元:
res.setHeader('Access-Control-Allow-Origin', '*');
然而若要支援 Cookie 或認證資訊,則不可使用 *,必須改用指定網域。
使用反向代理(Proxy Server)
瀏覽器的同源政策只在「瀏覽器端」發送請求時才會檢查。若在後端、或命令列工具上發送跨域請求,其實就不會被 CORS 限制。
因此,一個常見的做法是建置一個「反向代理」,在本機或同源伺服器上啟動一個中繼站,瀏覽器只要連到此中繼站(同源,所以不會被擋),由代理伺服器再幫你轉送到真正的目標 API,並將回應轉發回來。在這過程中,瀏覽器只看見「同源請求」,自然不會有 CORS 的麻煩。
範例
graph LR
A[Client Browser] -- 同源請求 --> B[Local Proxy Server] -- 對外請求 --> C[Real Server]
C[Real Server] -- 回應 --> B[Local Proxy Server] -- 回應 --> A[Client Browser]
在前端的設定只要把 API 位置寫成 Proxy Server(相同網域),代理端再把目標伺服器回傳的資料加上 Access-Control-Allow-Origin:* 或是其他合適的標頭,就能避開瀏覽器的限制。
開發時常用替代方案:瀏覽器插件或關閉安全策略
瀏覽器插件
- Chrome Web Store 有許多「Allow CORS」類型的插件,一鍵打開後,就能讓瀏覽器略過安全檢查或自動加上 CORS Header。開發測試時很方便,但務必記得在正式使用時關閉,以免破壞瀏覽器原生的安全防護。
關閉安全策略
- 例如在 Chrome,使用特殊啟動參數 –disable-web-security –user-data-dir=somePath 可完全關閉同源政策。但此方法極不安全,只適合短暫的開發測試環境,且建議使用獨立的 User Profile 以免影響日常瀏覽。
在 Express 中設定 CORS
Express 官方有一個 cors 中介軟體,可以快速管理 CORS 的設定:
npm install cors
const express = require('express');
const cors = require('cors');
const app = express();
const corsOptions = {
origin: ['http://localhost:3000', 'https://www.example.com'],
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // 若要允許攜帶Cookie
// maxAge: 86400, // 預檢快取時間(秒)
};
app.use(cors(corsOptions));
app.get('/api/data', (req, res) => {
res.json({ message: 'Hello from server' });
});
app.listen(8080, () => console.log('Server is running on port 8080'));
如此一來,瀏覽器端只要來源網域符合 origin 設定,即可成功取得回應。
在 Vite / Webpack-dev-server 中使用 Proxy
Vite、Webpack-dev-server 或類似開發伺服器,也能內建反向代理設定:
- Vite:在 vite.config.js 裡的 server.proxy 屬性設定。
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://real-server.com',
changeOrigin: true, // 是否修改來源
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
- Webpack-dev-server:在 webpack.config.js 的 devServer.proxy 屬性中配置。
此類代理在開發階段非常好用,讓你的前端請求看起來像是同源 /api 開頭,其實最終會轉發到不同網域或不同主機上。
與 JSONP 的比較
JSONP(JSON with Padding)是一種早期為了跨域取資料而發明的技巧,利用 <script> 標籤可以任意載入外部 JavaScript 這個特性,並在伺服器回傳可執行的 JS 內容再呼叫客戶端指定的 callback。
然而,JSONP 只能使用 GET 方法,也無法覆蓋更多複雜的跨域情況。CORS 是正式的 W3C 標準,支援各種 HTTP 方法、標頭與 Cookie 憑證。以現代開發而言,CORS 幾乎取代了 JSONP,除非遇到非常老舊瀏覽器或一些特殊場景,才會考慮 JSONP。
最佳實踐與安全考量
- 避免設定 Access-Control-Allow-Origin: *
若需要 Cookie 或更高安全需求,請明確列出可允許的網域清單。 - 小心 null 來源
在有些情況(如本地檔案 file:// 或 sandbox iframe)會出現 Origin: null,請勿隨意將 null 列入白名單,以防被惡意利用。 - 設定 Access-Control-Max-Age
減少頻繁的預檢請求,提高性能,但也要依據風險考慮太長的快取時間。 - 搭配 CSRF 防護
CORS 只是用來限制不受信任的來源,若同網域的攻擊手法(如 XSS)仍可能導致 CSRF;因此亦需要其他防護手段,如 CSRF token。 - 清晰的錯誤日誌與除錯
當 CORS 出錯,瀏覽器 Console 通常會出現類似 No ‘Access-Control-Allow-Origin’ header is present on the requested resource. 錯誤訊息。務必善用開發者工具檢查 Request/Response Header 是否符合預期。
CORS 核心標頭對照表
下表整理了 CORS 流程中常用的 HTTP Header,與其功能摘要:
Header 名稱 | 方向 | 範圍(何時出現) | 功能與用途 |
---|---|---|---|
Origin | Request | 簡單請求、非簡單請求 | 瀏覽器自動帶上,指明本次請求的網頁來源(協定+網域+Port),用於伺服器判斷是否允許跨域。 |
Access-Control-Request-Method | Request | 預檢請求(非簡單) | 指示瀏覽器即將使用哪種方法(如 PUT、DELETE),讓伺服器判斷是否允許。 |
Access-Control-Request-Headers | Request | 預檢請求(非簡單) | 列出自定義的標頭清單,例如 Content-Type, X-Custom-Header,供伺服器判斷是否允許。 |
Access-Control-Allow-Origin | Response | 任意 CORS 回應 | 伺服器回應指定允許存取的來源,或 代表皆可。若要攜帶憑證,不可用 。 |
Access-Control-Allow-Methods | Response | 預檢回應(非簡單) | 伺服器回應允許使用的 HTTP 方法清單,例如 GET, POST, PUT, DELETE。 |
Access-Control-Allow-Headers | Response | 預檢回應(非簡單) | 伺服器回應允許使用的自定義標頭清單。若請求帶有自定義標頭,這裡需要列出。 |
Access-Control-Allow-Credentials | Response | 預檢回應、簡單回應 | 是否允許跨域帶上 Cookie / 認證,值為 true 或省略不設定。若為 true,Access-Control-Allow-Origin 不可為 *。 |
Access-Control-Max-Age | Response | 預檢回應(非簡單) | 秒數,瀏覽器得以快取預檢結果的時長,期間內相同跨域條件的後續請求可免預檢,提升效能。 |
Access-Control-Expose-Headers | Response | CORS 回應 | 允許前端使用 getResponseHeader() 等方式存取更多非基本標頭,例如 X-Custom-Header。 |
常見問題(FAQ)
Q:如果在前端加上 Access-Control-Allow-Origin 頭就能跨域嗎?
A:不行。Access-Control-Allow-Origin 必須由伺服器回應帶上才有效果。前端自行添加無用;CORS 的核心在於伺服器端對跨域來源的授權。
Q:為什麼我明明在後端 Access-Control-Allow-Origin: ,但是還是無法帶上 Cookie?
A:如果要跨域攜帶 Cookie,Access-Control-Allow-Origin 不能使用萬用字元 ,而必須明確指定來源,並且需搭配 Access-Control-Allow-Credentials: true 與前端的 fetch 或 XHR 打開 credentials / withCredentials。
Q:使用 fetch 時常見的 mode: ‘no-cors’ 有什麼用途?
A:mode: ‘no-cors’ 會造成瀏覽器不會報 CORS 錯誤,但同時也拿不到實際的回應內容。這一般只用於特定情況,實務上若要跨域且能讀取回應,需使用 mode: ‘cors’。
Q:跨域請求時,後端收到的請求其實已經執行了,那瀏覽器為什麼還會報錯?
A:同源政策只擋「瀏覽器對回應的存取」,並不會阻止伺服器收到請求並執行。因此即使瀏覽器最後報錯,但後端可能已經執行了相應操作。所以若要防止惡意操作,伺服器端也需檢查來源,或使用適當的驗證機制。
Q:可以一次允許多個不同網域嗎?
A:Access-Control-Allow-Origin 不能同時列出多個網域值,也不能用逗號列出多個。若需求上需要,通常是在後端程式根據 Origin 內容去判斷,如果符合白名單就回覆 Access-Control-Allow-Origin: <該Origin>,或者是使用萬用字元(但不適用於憑證)。
總結
CORS 是現代 Web 開發中不可或缺的一項安全機制,透過「同源政策」搭配「跨來源資源共用」,為前端應用與後端伺服器在跨網域情況下取得資料提供了可控且安全的作法。CORS 之所以被廣泛使用,正是因為它讓開發者能夠在分散的微服務環境、前後端分離或與第三方 API 交互時仍保持安全的資料傳遞。
在實務上,解決 CORS 的手段主要圍繞在後端設定 Access-Control-Allow-* 相關欄位,或在開發端加上代理轉發。此外,也有許多建置與除錯的小技巧,如利用瀏覽器插件、暫時關閉同源策略等,但這些僅適用於開發階段,正式上線時還是建議在伺服器端正確配置 CORS。
同時,千萬不要忽視 CORS 與其他安全機制(像是 CSRF token、OAuth 等)之間的配合,綜合多方面的保護才能真正降低風險。
在深入理解之後,你會發現 CORS 雖然看似繁瑣,但其背後是瀏覽器與網路安全發展下的重要里程碑。只要正確設置與運用,就能在合法情境下方便地跨網域整合 API 與資源,並維持整體的安全性。
資料來源
- 前端如何處理 CORS. 幹前端或多或少都撞過 CORS 問題,雖然未必都會碰上,但也是早晚的問題。 | by Lastor | Code 隨筆放置場 | Medium
- CORS 是什麼? 為什麼要有 CORS?|ExplainThis
- 深入了解 CORS (跨來源資源共用): 如何正確設定 CORS? – Shubo 的程式開發筆記