你可能遇過這種情況:後端 API 在瀏覽器直接開得出來,用 Postman 或 curl 呼叫也能得到正常資料,但前端網站一執行 fetch(),Console 卻出現一大串紅字:
Access to fetch at ... has been blocked by CORS policy.
第一個直覺通常是:「API 掛了嗎?」
其實很多時候,API 完全正常。真正拒絕讓 JavaScript 讀取回應的,是瀏覽器。
這背後牽涉到 Web 很重要的一套安全邊界:Same-Origin Policy,也就是同源政策。而 CORS(Cross-Origin Resource Sharing,跨來源資源共享)不是用來「阻擋跨網域」的技術;更精確地說,它是一套讓伺服器告訴瀏覽器「哪些跨來源存取可以被允許」的機制。
先理解 Origin:什麼叫「同一個來源」?
瀏覽器判斷兩個網址是不是同源,不是只看網域名稱。Origin 通常由三個部分組成:
scheme(協定)+ host(主機)+ port(連接埠)。
例如:
https://example.com:443
可以拆成:
scheme:https
host:example.com
port:443
如果其中任何一項不同,瀏覽器就可能把它視為不同 Origin。
因此:
https://example.com 與 http://example.com 不同源,因為 scheme 不同。
https://app.example.com 與 https://api.example.com 也不同源,因為 host 不同。
http://localhost:3000 與 http://localhost:8000 同樣不同源,因為 port 不同。
這也是為什麼前後端分開開發時特別容易第一次撞上 CORS。你的 React、Vue 或其他前端開發伺服器可能跑在 localhost:3000,而 API 跑在 localhost:8000;即使它們都在同一台電腦,對瀏覽器來說仍然是兩個 Origin。
為什麼瀏覽器要有 Same-Origin Policy?
假設沒有同源政策。
你登入銀行網站後,瀏覽器裡可能還保存著銀行的登入 Cookie。接著你打開一個惡意網站。如果那個網站的 JavaScript 可以毫無限制地向銀行網站發送請求並讀取回傳內容,它就可能偷偷取得只有登入者才能看到的資料。
因此瀏覽器會建立一條重要的隔離邊界:某個 Origin 載入的 JavaScript,不能任意讀取另一個 Origin 的敏感資源。
這就是 Same-Origin Policy 的核心目的。
需要注意的是,「不能讀取」不等於「完全不能送出請求」。Web 上有很多歷史悠久的跨來源行為,例如圖片、表單、script 等都有自己的規則。因此討論 CORS 時,不能簡化成「瀏覽器禁止所有跨網域連線」。真正重要的是:瀏覽器是否允許目前這段前端 JavaScript 存取跨來源回應。
那 CORS 到底做什麼?
現代網站很常把前端和 API 放在不同 Origin。
例如:
前端:https://app.example.com
API:https://api.example.com
如果瀏覽器一律禁止 JavaScript 讀取跨來源 API,正常的前後端分離架構就會非常難使用。
所以有了 CORS。
CORS 的概念可以理解成:API 伺服器透過 HTTP Response Header,向瀏覽器宣告哪些 Origin 可以存取它。
最常看到的 Header 是:
Access-Control-Allow-Origin
例如 API 回傳:
Access-Control-Allow-Origin: https://app.example.com
代表伺服器告訴瀏覽器:「我允許 https://app.example.com 這個 Origin 的前端程式讀取這個回應。」
如果瀏覽器送來的 Origin 不在允許範圍內,瀏覽器就不會把回應開放給該 JavaScript 使用。
所以 CORS 的權限設定通常是在伺服器端,而不是前端用某個神奇的 fetch 參數就能解除。
一次普通的跨來源請求發生了什麼?
假設網站:
https://learn.example.com
要呼叫:
https://api.example.com/articles
瀏覽器送出請求時,可能帶上:
Origin: https://learn.example.com
API 收到後,如果允許這個來源,就在回應加入:
Access-Control-Allow-Origin: https://learn.example.com
瀏覽器檢查後發現符合規則,才讓前端 JavaScript 正常取得 response。
這裡有一個容易搞混的地方:執行 CORS 規則的主要角色是瀏覽器。
這也是為什麼同一個 API 用 curl、Postman 或後端程式呼叫時可能完全沒問題。那些環境通常不需要像瀏覽器一樣執行 Web 頁面的 Same-Origin Policy。
因此「Postman 可以,瀏覽器不行」反而是非常典型的 CORS 線索。
Preflight:為什麼有時候會先出現 OPTIONS?
有些跨來源請求在真正送出之前,瀏覽器會先問一次伺服器:「等等我要送這種請求,你允許嗎?」
這個預先詢問叫做 Preflight Request,通常使用 HTTP OPTIONS method。
例如前端準備送出:
PUT /users/123
Content-Type: application/json
Authorization: Bearer ...
瀏覽器可能先送:
OPTIONS /users/123
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
伺服器如果允許,就可能回覆:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Authorization, Content-Type
瀏覽器確認規則通過後,才送真正的 PUT request。
所以你在後端 log 裡看到莫名其妙的 OPTIONS,不一定是有人亂打 API;它可能只是瀏覽器正在做 CORS Preflight。
為什麼有些請求不用 Preflight?
為了相容早期 Web 行為,CORS 定義了一類條件較受限制的「simple request」。符合條件的 GET、HEAD 或 POST 等請求,在特定 method、header 與 Content-Type 條件下,可以直接送出,不必先做 Preflight。
但「不用 Preflight」不代表「不用 CORS」。
瀏覽器仍然會檢查 API 回傳的 CORS headers,決定 JavaScript 能不能讀取 response。
這個差異很重要:Preflight 只是 CORS 流程中的其中一種機制,不等於 CORS 本身。
Access-Control-Allow-Origin: * 可以全部解決嗎?
你可能看過最省事的設定:
Access-Control-Allow-Origin: *
星號代表允許任意 Origin 存取符合條件的資源。對真正公開、不依賴使用者身分的 API,這可能是合理選擇。
但它不是萬用修復鍵。
如果 API 涉及登入狀態、Cookie 或其他 credentials,規則會更嚴格。更重要的是,如果資料本來就只應提供給特定前端,把所有 Origin 都放行會讓你的安全邊界比需求更寬。
實務上應該先回答:「哪些網站真的需要從瀏覽器讀取這個 API?」再設定 allowlist,而不是看到 CORS error 就直接放 *。
Cookie 與 Credentials 為什麼更麻煩?
如果跨來源 request 需要帶 Cookie,前端可能會使用:
fetch(url, {
credentials: 'include'
})
伺服器則需要正確處理對應的 CORS credentials 規則,例如:
Access-Control-Allow-Credentials: true
此時 Access-Control-Allow-Origin 不能單純使用 * 來代表任意來源,而通常需要回傳明確允許的 Origin。
此外,Cookie 本身還受到 SameSite、Secure、Domain 等屬性影響。因此「Cookie 沒有被送出去」不一定只是一個 CORS 問題。
這也是實務除錯時很常見的陷阱:CORS、Cookie policy、authentication 是不同層次的機制,錯誤現象卻可能看起來很像。
CORS 不是 Authentication,也不是 Authorization
這點非常重要。
CORS 不能取代登入驗證,也不能決定使用者是否有權讀取某筆資料。
假設 API 設定:
Access-Control-Allow-Origin: https://app.example.com
這只代表瀏覽器允許該 Origin 的前端程式讀取 response。它不代表所有從這個網站送出的 request 都是可信使用者,也不代表 API 可以跳過 token、session 或權限檢查。
Origin 不是使用者身分。
真正的 API 仍然需要 Authentication 與 Authorization。
同樣地,CORS 也不是用來防止別人從自己的伺服器呼叫你的 API。攻擊者完全可以寫後端程式直接送 HTTP request,而不受瀏覽器 Same-Origin Policy 約束。
所以如果你想限制 API 使用量,應該使用 authentication、authorization、rate limiting 等機制,而不是把 CORS 當成 API 防火牆。
CORS Error 有時候其實是別的錯
瀏覽器顯示 CORS error 時,不要立刻認定「一定是 allow-origin 少寫一行」。
例如:
API 發生 500,但錯誤回應沒有附 CORS header;
Reverse Proxy 沒有正確處理 OPTIONS;
重新導向到另一個 Origin;
CDN 回傳的錯誤頁沒有原本 API 的 headers;
TLS 或網路問題讓請求根本沒有正常完成。
最後在瀏覽器裡都可能呈現出很像 CORS 的症狀。
因此除錯時,應該打開 DevTools 的 Network 面板,查看真正送出了哪些 request、OPTIONS 是否成功、response status 是多少,以及 response headers 到底有哪些。
只盯著 Console 最後那一行紅字,往往資訊不夠。
Production 中應該怎麼設定?
一個比較合理的做法是把允許的 Origin 明確列出。
例如 production 只允許:
https://app.example.com
開發環境再額外允許:
http://localhost:3000
後端收到 Origin 後,檢查是否位於 allowlist;符合才回傳相對應的 Access-Control-Allow-Origin。
如果支援多個 Origin,也不要隨便把任何傳入的 Origin 原樣反射回去。否則看起來像 allowlist,實際上等於誰來都允許。
同時只開放真正需要的 methods 與 headers。例如 API 根本不使用 DELETE,就沒有必要為了「避免出錯」而把所有 method 全開。
原則仍然是最小權限:只允許系統實際需要的跨來源能力。
Reverse Proxy 能幫忙避開 CORS 嗎?
可以,而且這是常見架構之一。
假設使用者只看到:
https://example.com
前端資源放在:
https://example.com/
API 對外則透過 Reverse Proxy 暴露成:
https://example.com/api/
瀏覽器看到的 scheme、host 與 port 都相同,因此前端呼叫 /api/users 時可能根本不屬於 cross-origin request。
Reverse Proxy 再在伺服器內部把 /api/ 轉送到真正的 backend service。
這不代表 CORS 沒有價值,而是架構把瀏覽器看到的入口整理成同一個 Origin,讓一部分跨來源複雜度自然消失。
一個完整流程
假設:
Frontend:https://app.example.com
Backend:https://api.example.com
前端要送帶 Authorization header 的 POST request。
整個流程可能是:
- JavaScript 呼叫 fetch()。
- 瀏覽器判斷這是 cross-origin request。
- 因為 request 不符合 simple request 條件,瀏覽器先送 OPTIONS Preflight。
- Backend 檢查 Origin、method 與 requested headers。
- Backend 回傳允許的 CORS headers。
- 瀏覽器確認 Preflight 通過。
- 瀏覽器送出真正的 POST request。
- Backend 執行 authentication、authorization 與業務邏輯。
- Backend 回傳資料與必要的 CORS headers。
- 瀏覽器再次檢查後,才把 response 交給 JavaScript。
把這十步分清楚之後,很多「神祕的 CORS 問題」其實都可以定位到某一層。
最後整理
CORS 最容易被誤解成一個麻煩的瀏覽器限制,但它其實是在 Same-Origin Policy 的安全模型上,提供一個受控的跨來源開口。
你可以記住幾件事:
Origin 由 scheme、host、port 決定;其中一項不同,就可能是 cross-origin。
Same-Origin Policy 是瀏覽器的重要安全邊界。
CORS 由伺服器透過 HTTP headers 宣告哪些跨來源讀取可以被瀏覽器接受。
Preflight 是瀏覽器在某些 request 前用 OPTIONS 先確認權限,不是所有 request 都會出現。
CORS 不是登入、不是 API 權限控制,也不是防止伺服器端程式呼叫 API 的機制。
遇到錯誤時,應該從 Network 面板檢查 Origin、OPTIONS、status code 與 response headers,而不是直接把 Access-Control-Allow-Origin 改成 *。
當你理解 CORS 之後,前後端分離時那串看似莫名其妙的紅字,就不再是一道「瀏覽器不讓我連 API」的牆,而是一套可以逐層檢查的 Web 安全協議。
把概念放回真正的系統流程裡看,通常比只背名詞更容易理解它。