
CORS error message
前言
原本在處理前端 Next.js 跟後端 Node.js 之間的 fetch 資料傳輸,以為會碰到 CORS 的問題,但是後來發現在伺服器之間的 API http 通訊之間並不會觸發 CORS 限制,因為 CORS 是瀏覽器保護使用者的一種安全機制,如果這種專門給 Server 使用的 API 如果需要安全檢查,就要用 Server-to-Server API 專用的安全檢查方式,如 API Key 、 Shared Secret + HMAC 、 JWT 驗證 、 OAuth 2.0 Client Credentials(常見於企業級或第三方服務整合)等等方式。不過這也表示, 只要 API 本身沒有實作任何身分驗證或流量控管機制 ,即使 API 它是「設計給瀏覽器使用」且已正確設定 CORS ,但是只要它沒有實作任何「請求來源驗證、身分驗證或流量控管」機制,任何能發送 HTTP Request 的程式(爬蟲、bot、惡意腳本)都可以直接呼叫它,並進行攻擊。
從伺服器角度來看,HTTP Request 並不區分是否來自瀏覽器,因此僅依賴 CORS 並不能視為 API 安全防護。
簡介
什麼是同源政策(Same-Origin Policy, SOP)?
因為瀏覽器有同源政策(SOP, Same-Origin Policy)的緣故,其原由已經在 前一篇文章詳述了 ,所以這裡並不會再詳述細節。
所以當瀏覽器要存取跨網頁網域資源時,就需要用到跨來源資源共享 CORS(Cross-Origin Resource Sharing)機制。
瀏覽器會將來源(Origin)定義為三個組合:
- 協定 (protocol), ex: http, https
- 網域 (domain), ex: example.com
- 連接埠 (port), ex: 80, 443
只要以上三者其中一項不同,就會被瀏覽器視為不同來源 (cross-origin) 。
在 SOP 的限制下, 瀏覽器預設禁止網頁直接讀取不同來源的資源內容 ,以防止惡意網站竊取使用者資料。
為什麼需要 CORS ?
然而,並非所有跨源存取都是惡意行為,例如:
然而,並非所有跨源存取都是惡意行為,例如:
- 前端呼叫自家後端 API
- 使用 CDN、第三方 API
- 前後端分離架構(SPA + API)
因此,瀏覽器提供了 **CORS(Cross-Origin Resource Sharing)** 這套機制,讓後端伺服器可以「明確告訴瀏覽器」:
那些來源是可以合法存取這些資源
CORS 的歷史背景簡介
在 CORS 出現之前,開發者曾使用過一種替代方案: JSONP(JSON with Padding) 。
JSONP 的原理是透過 <script> 標籤載入遠端 JavaScript,藉此繞過同源政策限制。但由於它本質上是執行遠端程式碼,因此在安全性與可控性上存在風險,也容易成為 XSS 攻擊的載體。
JSONP 只能使用 GET,因為 <script src="..."> 本質上無法攜帶 request body,也無法自訂 HTTP method,所以使用上也比較複雜。
隨著 Web 應用日益複雜,以及 CORS 的出現,這種方式逐漸被淘汰。
CORS 的相關概念在 2000 年代中期開始被提出,並於 2010 年前後由 W3C 整理為正式規範,之後逐步被主流瀏覽器全面支援,成為現代 Web 開發的標準做法。
CORS 的角色分工
CORS 的限制與判斷,實際上是由瀏覽器執行的。
前端瀏覽器負責依 CORS 規範判斷請求流程
瀏覽器會根據 HTTP Request 內容判斷是否為同源,如果為非同源就會走 CORS 流程,之後瀏覽器會根據該 Request 內容是否符合 Simple Request 的條件,不符合則會在正式發送 Request 以前先向後端伺服器發送 Preflight (OPTIONS) Request ,以判斷後端伺服器是否接受該 CORS Request
是否允許該跨來源請求攜帶 credential (如 cookies、Authorization header) ,以及是否允許 JavaScript 存取回傳的 response。
後端伺服器的 HTTP Response 是否有做 Preflight(OPTIONS) HTTP Response Header 處理,會影響到瀏覽器收到 Response 後判斷該 Response 是否為合法的 CORS Response , JavaScript 是否可以處理 HTTP Response 跟 Cookies 內容。
後端伺服器負責角色
後端伺服器的腳色,僅僅只是在 HTTP Response 中回傳 CORS Header ,處理 CORS Preflight
瀏覽器收到 Response 後,才會根據這些 Header 決定:
- 是否允許 JavaScript 讀取 response
- 是否要在 console 顯示 CORS error
以下以 Node.js express 先不使用 cors 這個套件處理,純手寫的方式,展示基本的流程,尚未觸及更複雜的 credential ,並以註解說明各段程式碼在後端的 CORS 處理的作用是什麼,以清楚說明後端對於 CORS 處理的細節。
不過實務上並不建議用純手寫的方式來處理後端的 CORS ,建議以專用套件來處理 CORS 流程。純手寫的除了程式碼攏長,處理各個功能流程邏輯複雜,還容易一不小心忽略必要的 Header 處理,導致到時要 debug 很久。
import express from "express";
const app = express();
const port = 2000;
// CORS middleware
// 所有的 http response 都會帶有這些 CORS 相關的 header
app.use((req, res, next) => {
// 後端回傳 response header CORS 允許的網域,]
// * 代表所有網域,可以指定一個網域,如: https://example.com ,如需要多個白名單,在後面 CORS 套件會詳述方法
// 不過如果是要用 credentials (cookies) 要指定明確的網域,不能用 *
// 除非是對外自由開放的 API ,不然實務上並不建議使用 * ,通常會搭配白名單動態判斷 Origin 來源是否為允許的來源。
res.setHeader("Access-Control-Allow-Origin", "*");
// 後端告訴瀏覽器:允許的 HTTP 方法
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
// 這段是在處理非 simple request 時候,
// 會先送 Preflight(OPTIONS)詢問是否允許使用這些 HTTP header
// 瀏覽器會比對 Access-Control-Request-Headers
// 與後端回傳的 Access-Control-Allow-Headers 是否相符
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
// 如果是 CORS Preflight (OPTIONS),便會在此結束,並回傳帶有以上程式碼後端所給的 http response header
// 給瀏覽器檢查後端回傳的規則,讓瀏覽器決定是否要發出真正的 request
// 此時不進入任何 API 邏輯,直接回成功狀態即可
// OPTIONS 沒處理 → 404
if (req.method === "OPTIONS") {
// 使用 204(No Content)是因為 Preflight 只需要 header,不需要 response body。
// 因為早期瀏覽器 對於 HTTP Status Code 支援度不佳
// 如過是部分的系統可能會回 200 以求最大相容性
return res.sendStatus(204);
}
next();
});
// 後端 API
app.get("/", (req, res) => {
res.send("Server is running!");
});
app.listen(port, () => {
console.log(`Server running on ${port}`);
});
⚠ 如果 HTTP Request 不是由瀏覽器發起, (例如 Server-to-Server、curl、Postman),就根本不會有 CORS 這一層限制。如果後端 API 需要防禦這些非瀏覽器發起的,或是防禦非預期 HTTP Request ,可以用以下防禦策略:
瀏覽器威脅(跨站)
- CSRF Token
- SameSite Cookie
非瀏覽器 / 機器人 / 服務
- Authorization Header(JWT)
- 自訂 Header + 驗證
- Rate limit / Firewall(API 防禦機制)
CORS 不是伺服器安全機制
CORS 是瀏覽器用於保護使用者的一種安全機制,不是伺服器的 API 安全機制例如:爬蟲、 curl 、 postman 或是網路攻擊工具依然可以發出大量的 HTTP Request 到後端伺服器的 API 上面去。伺服器端要自己實作 API 安全的身分驗證、授權與流量控管,必要時可以搭配防火牆做安全防禦。
CORS 的機制
CORS 是瀏覽器在「收到 response 之後」決定要不要交給 JavaScript 使用的規則,而不是伺服器用來拒絕請求的防火牆。
CORS 分為 Simple Request 和 Preflight (OPTIONS) Reques 兩種流程,只是 CORS 的請求流程分類,本身並不決定是否攜帶 Cookies 。是否會攜帶 Cookies,取決於瀏覽器端的 credentials 設定,以及後端回傳的 CORS response header 與 cookie 的 SameSite 屬性。
至於 CORS 的 cookies 前後端操作方法會在後面統一解釋,因為這部分是個大坑。
Simple Request
Simple Request 是「為了不破壞舊網站而保留的特例」,不是效能最佳化設計,少一次 RTT 單純是額外的附加效果
雖然 Simple Request 可以少一次 Preflight(OPTIONS) ,少一次 RTT(Round-Trip Time) ,讓網頁反應時間變快,但其出現的關鍵原因是為了相容在 CORS 出現以前的 Web 生態功能,反應時間變快,只是其出現的附加作用。
CORS 出現以前(2000 年前後)的 Web 世界, 那個時代即使有 JavaScript,但是尚未有 XMLHttpRequest 與現代意義的 AJAX,JavaScript 幾乎無法主動發送與處理 HTTP Request / Response。,沒有動態網頁,所有的網頁全是以 HTML 功能向後端伺服器送出 Request 後,後端伺服器在處理完畢後回傳整頁的 HTML 網頁,或是回傳指定網址跳轉資訊。
那時候的 HTTP 互動完全依賴 HTML 的以下標籤互動:
<img src="http://example.com/image.jpg" />
<form action="https://example.com/transfer" method="POST"></form>
<link rel="stylesheet" href="http://example.com/style.css" />
<script src="http://example.com/lib.js"></script>
<iframe src="...">
預設全是可以跨源請求,只要 domain 相同就會自動攜帶 cookies,尚未有 SameSite 與 CORS 的限制概念。
Simple Request 的設計多是為了相容舊時代 Web 的行為而設計的。
⚠ Simple Request 並不是由「HTML Element 或 JavaScript fetch」來區分,而是由瀏覽器判斷 Request 是否符合 Simple Request 的條件來判斷。
雖然 Simple Request 的設計目的是為了相容舊時代由 HTML Element(如 form、img)所產生的請求行為,但現代瀏覽器中,由 JavaScript fetch 發起的 Request,只要符合 Simple Request 的規範條件,同樣會被視為 Simple Request,而不會觸發 Preflight (OPTIONS)。
以下為一個符合 Simple Request 的 fetch 範例,模擬發送 form element http request:
fetch("https://api.example.com/data", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: "a=1&b=2",
});
Simple Request 的瀏覽器檢查規則:
Method 只能為: GET 、 POST 、 HEAD
Request Header 只能使用 Simple Header :
Accept
Accept-Language
Content-Language
Content-Type(但有限制,見下)
Content-Type(如果有)只能是以下三種:
application/x-www-form-urlencoded
multipart/form-data
text/plain
只要以上三個條件都符合,瀏覽器的 CORS 就會走 Simple Request 。
Simple Request 流程:
CORS Request 符合 Simple Request
|
| 瀏覽器送出真正的 Request
|
▼
Server
|
| 返回 Response ,並在 header 包含 CORS 規則
|
▼
瀏覽器檢查 Response header ,決定是否要處理該 Response ;決定 JavaScript 能否讀取 Response 內容
Preflight (OPTIONS) Request
reflight 的本質就是「在發送 HTTP Request 前,瀏覽器先幫你問一次:這樣做行不行?」
當瀏覽器要送出 CORS Request 時候,檢查不符合 Simple Request 規則時候,就會走 Preflight (OPTIONS) Request 流程。這時會瀏覽器會先向後端伺服器先送出 Preflight (OPTIONS) Request ,之後檢查返回的 Request http cors header ,檢查規則,是否與即將送出的真正的 Request CORS 相符符合伺服器規則的話就送出真正的 Request ,不符合的話,就不發送真正的 Request ,並顯示 CORS Error 訊息。
Preflight 的 http method 是 OPTIONS ,作用是詢問後端伺服器是否支援否某些操作,在決定下一步驟要如何。
其 OPTIONS 內容會類似如下內容
OPTIONS /api/login
Origin: https://frontend.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
整個流程會是如以下:
CORS Request 不符合 Simple Request
│
│ OPTIONS 第一個 RTT
│
▼
Server
│
│ OPTIONS response 會包含 Server 支援的 CORS 操作項目
│
▼
Browser -----------------> 瀏覽器比對不符合 Server 的 CORS 操作,停止發送真正的 Request ,並顯示 CORS Error 訊息。
│
│ 真正 request 第 2 個 RTT
│
▼
Server
│
│ response
│
▼
Browser
CORS 的 Cookies 操作(大坑)
Cookies 的設定較為複雜,其中還牽扯後端伺服器產生 Cookies 是否允許跨站,還有 CORS 設定是否允許 Credentials ,前端預設 CORS 不會攜帶 Credentials ,以及前端的 Credentials 是否為 include 等等設定。這邊會從後端產生 Cookies 設定開始說起。
後端產生 Cookies 時候就要設定可以跨網域攜帶
我先以 node.js express 示範一段後端是怎麼後端伺服器是怎麼設定 Cookies 規則的。
import express from "express";
import cookieParser from "cookie-parser";
const app = express();
const port = 2000;
// 解析 Cookie(req.cookies)
app.use(cookieParser());
app.get("/set-cookie", (req, res) => {
res.cookie("session_id", "abc123", {
httpOnly: true, // 瀏覽器端的 JavaScript (如 document.cookie) 無法讀取
secure: true, // 只允許在 HTTPS 連線時傳送 cookie , false 則是 http 跟 https 都會傳送 cookie
sameSite: "lax", // 限制跨站請求攜帶 cookie(允許使用者主動導覽的 GET), sameSite 設定後面會有一小節專門說明跨站設定
maxAge: 1000 * 60 * 60, // cookie 從現在起 1 小時後過期(毫秒)
path: "/", // 這個 cookie 在那些 URL 路徑會發送,當為 / 時候代表整個網站所有路徑都會攜帶此 cookie
});
res.json({ message: "Cookie set" });
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
Cookie 的跨站設定重點在 sameSite 這個參數的值,可用設定分別詳述如下:
- lax: 限制跨站請求攜帶 cookie ,但是允許使用者主動導覽的 GET 跨站攜帶。
- strict: 嚴格禁止跨網域攜帶 cookie 。
- none: 允許跨網域攜帶。
補充: cookies 的 SameSite 是後來補的洞,因為早期 Simple Request 不能擋,且會攜帶 cookies ,所以就在 cookies 層阻擋。在非常早期的 Simple Request 幾乎不管是不是跨域,都會攜帶 cookies ,但是在現代瀏覽器則是會根據 SameSite 決定跨域可不可以攜帶 cookies 。
重要補充: 在現代瀏覽器(Chrome 80+)規定,如果設定 SameSite: "none",除非網域是 localhost ,否則必須同時加上 Secure: true(即必須透過 HTTPS),否則瀏覽器會拒絕存取該 Cookie。
額外補充: 在網址為 localhost 的本機測試環境,瀏覽器會允許使用 http 攜帶 Cookies ,但是在其他非 localhost 網域自從 2020 年 Chrome 80+ 版本開始以後的瀏覽器,就強制要使用 https ,也就是 secure: true 才允許攜帶 Cookies ,若有非 localhost 的測試需求,建議使用 Reverse Proxy 搭配 SSL 憑證(如 mkcert)來模擬真實的 https 環境。
Cookies 的跨域要先在後端產生 Cookies 時候,就在 Cookies 層設定 sameSite: 'none' 才能在 CORS 時候攜帶後端的 CORS 攜帶 Cookies 的設定
// 這裡先不討論多網域的白名單處理,先不用套件的方式逐一說明,後端伺服器是如何處理 CORS Cookies
// CORS middleware
app.use((req, res, next) => {
// Access-Control-Allow-Origin 不能為 * ,必須明確指定晚整的網域
res.setHeader("Access-Control-Allow-Origin", "https://frontend.example.com");
// Credentials 必須為 true , CORS 規則才會允許攜帶 cookies
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
// Preflight(OPTIONS)直接回傳 204
// OPTIONS 沒處理 → 404
if (req.method === "OPTIONS") {
return res.sendStatus(204); // No Content
}
next();
});
前端 CORS 預設不會攜帶 cookies
這裡以 JavaScript fetch 來說明關於前端的 CORS Cookies 是如何處理的,重點在 fetch 的 credentials 這個參數,如果沒有這個參數,不管是同源,還是非同源,預設都不會在 Request 帶 Cookies 。
fetch("https://api.example.com/profile", {
credentials: "include",
});
在 credentials 有三個設定值,分別描述如下:
- include: 不管事同源還是非同源,都會帶 Cookies
- same-origin: 只有與網頁同源,才會帶 Cookies ,現代瀏覽器 fetch 預設設定。
- omit: 用於確保瀏覽器絕對不會帶 Cookies
Simple Request 又帶有 CORS Cookies (存在,但是不常去處理)
Simple Request 又帶有 CORS Cookies 這個條件實際上是存在的,因為早期瀏覽器在 SOP 出現以前,會在 Request 自動戴上 Cookies ,且也沒有檢查網域是否與網頁相同,所以才有這種特殊的作法存在。
大致原則就是瀏覽器會檢查 Cookies 的 SameSite 設定是否符合 Request 目標的同源或是跨源設定,符合就的時候,如該 Request 符合 Simple Request 標準,就會自動帶 Cookies ,這是為了相容舊時代的瀏覽器行為而保留的作法,不過如果後端沒有在 Response 明確對該 Cookies 做規則處理,通常瀏覽器會在收到 Response 後自動將 Cookie 捨去不予處理。
總結來說: 是否攜帶 cookies,與是否為 Simple Request 沒有直接因果關係 ,而是取決於:
- Cookie 的 SameSite 設定
- Request 是否符合瀏覽器允許攜帶 cookies 的條件
- (CORS 情境下) 前端 credentials 與後端 Allow-Credentials
後端以 cors 套件設定,以及前端 CORS fetch 有 cookies 綜合使用範例
後端程式碼
import express from "express";
// node.js CORS 處理套件
import cors from "cors";
// 要有該套件, node.js 才有辦法讀取 request 的 cookies
import cookieParser from "cookie-parser";
const app = express();
const port = 2000;
/**
* 你唯一允許跨域存取的前端網域
* ⚠️ 一定要是完整 origin(含 protocol + domain + port),沒有設定 port 就會用 protocol 預設的 port
*/
// 此為 VS code 的 Liveserver 預設網址
const FRONTEND_ORIGIN = "http://127.0.0.1:5500";
/**
* CORS 設定物件
*/
app.use(
cors({
/**
* 告訴瀏覽器:
* - 只允許這個特定 Origin 跨域存取
* - credentials: true 時,這裡「不能用 '*'」
*/
origin: FRONTEND_ORIGIN
/**
* 允許跨域攜帶 cookies / Authorization
* - 會讓後端回:
* Access-Control-Allow-Credentials: true
*/,
credentials: true
/**
* 允許的 HTTP methods(主要給 Preflight 用)
*/,
methods: ["GET", "POST", "PUT", "DELETE"]
/**
* 允許前端實際送出的 request headers
* - 非 simple request 要自訂允許的 header 欄位,要補在這裡
*/,
allowedHeaders: ["Content-Type", "Authorization"]
/**
* Preflight 快取時間(秒)
* - 瀏覽器在這段時間內不會一直重送 OPTIONS
*/,
maxAge: 86400
/**
* OPTIONS 成功時回傳的狀態碼
* - 204 = No Content(語意最正確)
* 因為早期瀏覽器 對於 HTTP Status Code 支援度不佳
* 如過是部分的系統可能會回 200 以求最大相容性
*/,
optionsSuccessStatus: 204,
})
);
/**
* 讓 Express 自動處理所有路徑的 OPTIONS(Preflight)
* - cors 套件會幫你回正確的 CORS headers
*/
app.options("*", cors());
/**
* 解析 cookies(只是為了讓你能讀 req.cookies)
* - 跟「能不能跨域帶 cookies」是兩件不同的事
*/
app.use(cookieParser());
/* ---------------- API 區 ---------------- */
/**
* 示範:後端設置 cookie
*
* ⚠️ 跨域 cookie 必要條件:
* 1) 後端:credentials: true
* 2) 前端:fetch credentials: 'include'
* 3) cookie:
* - sameSite: 'none'
* - secure: true(正式環境必須 HTTPS)
*/
app.get("/set-cookie", (req, res) => {
res.cookie("session_id", "abc123", {
httpOnly: true, // 前端 JS 讀不到(安全)
secure: false, // ⚠️ 本機 http 測試可以先用 false ,不過正式環境必須為 true
sameSite: "none", // 跨站/跨域 cookie 必備
maxAge: 1000 * 60 * 60, // 一小時,毫秒
path: "/",
});
res.json({ ok: true, message: "cookie set" });
});
/**
* 示範:需要 cookie 的 API
*/
app.get("/profile", (req, res) => {
const sessionId = req.cookies?.session_id;
res.json({ sessionId });
});
/**
* 健康檢查
*/
app.get("/", (req, res) => {
res.send("Server is running!");
});
app.listen(port, () => {
console.log(`Server running on ${port}`);
});
前端 fetch 帶 cookies 的程式碼
async function getProfile() {
try {
const response = await fetch("http://localhost:2000/profile", {
credentials: "include", // include 為要求跨域也要帶 cookies ,沒有該設定預設為 same-site
});
const data = await response.json();
console.log("Profile Data:", data);
} catch (error) {
console.error("CORS Error or Network Error:", error);
}
}
getProfile();
Debug Checklist(可執行檢查順序)
第一步:排除基礎連線問題
脫離瀏覽器測試: 先使用 Postman 或 curl 等非瀏覽器環境,直接呼叫 API。
- 如果 Postman 也不通:那是 API 本身邏輯或網路問題。
- 如果 Postman 會通:確定是瀏覽器的 CORS 限制,繼續往下。
- 確認 API 安全機制: 檢查後端是否需要 API Key 或 Authorization Header,確保不是因為沒給憑證而被伺服器拒絕。
第二步:判斷請求類型
檢查是否為 簡單請求 (Simple Request):
- 觀察 DevTools Network 分頁,如果沒有出現 `OPTIONS` 請求,就是簡單請求。
- 發生錯誤時: 請看 Console 訊息,檢查後端 `Access-Control-Allow-Origin` 是否包含前端網域。
檢查 預檢請求 (Preflight / OPTIONS):
- 如果有
OPTIONS請求但沒送出真正的 Request:檢查後端的Access-Control-Allow-Methods或Headers是否允許該次操作。 - 如果 OPTIONS 回傳 404: 代表後端沒有正確處理
OPTIONS方法(例如 Express 沒掛載cors中間件或沒寫app.options())。
第三步:Cookies 專題排查 (最常踩的坑)
後端 Cookie 設定:
SameSite是否設為 None ?- 若設為 None,
Secure是否有設為 true ?(非 localhost 網域,跟正式環境必需條件)。
前端 Request 設定
-
fetch或axios是否有加上credentials: "include" 。
後端接收檢查:
- 是否已安裝並使用
cookie-parser等套件解析 Cookie? - 嘗試印出
req.cookies確認伺服器層級是否有收到內容。
💡 小訣竅
遇到 CORS 錯誤時,一般在非瀏覽器環境先排除後端 API 非 CORS 問題後,通常 看瀏覽器 Console 的報錯訊息 多可以大概知道原因,它通常會直接告訴你是哪個 Header 不符合規則(例如:The 'Access-Control-Allow-Origin' header contains multiple values... )。之後在根據 console 訊息逐一檢查前後端的 CORS 設定,通常可以找到問題原因。
參考
簡單弄懂同源政策 (Same Origin Policy) 與跨網域 (CORS)



















