你可能遇過這種情況:後端 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。

整個流程可能是:

  1. JavaScript 呼叫 fetch()。
  2. 瀏覽器判斷這是 cross-origin request。
  3. 因為 request 不符合 simple request 條件,瀏覽器先送 OPTIONS Preflight。
  4. Backend 檢查 Origin、method 與 requested headers。
  5. Backend 回傳允許的 CORS headers。
  6. 瀏覽器確認 Preflight 通過。
  7. 瀏覽器送出真正的 POST request。
  8. Backend 執行 authentication、authorization 與業務邏輯。
  9. Backend 回傳資料與必要的 CORS headers。
  10. 瀏覽器再次檢查後,才把 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 安全協議。

把概念放回真正的系統流程裡看,通常比只背名詞更容易理解它。