為什麼「跨域」是個問題?
在開始講 CORS 之前,先退一步問一個更根本的問題:為什麼跨域會是問題?這個問題的答案,決定了你對整個機制的理解深度。
跨域問題的根源,在於瀏覽器有一個其他工具沒有的特性:它會自動帶上 Cookie。
想像這個情境:你登入了 facebook.com,瀏覽器幫你存了一個 session cookie。 接著你在另一個分頁開了一個陌生網站 evil.com。 這個頁面裡有一段 JavaScript,它偷偷發了一個請求到 facebook.com/api/messages。
問題來了:瀏覽器會自動把你的 Facebook Cookie 帶上去。 如果沒有任何限制,evil.com 的 JavaScript 就能用你的身份讀取你的私訊、甚至刪除你的帳號,完全不需要知道你的帳號密碼。
這就是 CSRF(Cross-Site Request Forgery,跨站請求偽造) 攻擊的原型。 而瀏覽器為了防範這件事,內建了 Same-Origin Policy(同源政策)。
注意這裡說的是「瀏覽器」。curl、Postman、Node.js、你的後端服務,這些工具不會自動帶 Cookie、 沒有登入狀態的概念、也不存在「使用者正在瀏覽其他網站」這種情境。 所以它們根本不需要 Same-Origin Policy,跨域問題從一開始就只存在於瀏覽器環境。
Same-Origin Policy:瀏覽器的裁判規則
Same-Origin Policy 是瀏覽器廠商(Chrome、Firefox、Safari)出廠就內建的安全規則, 不需要任何人設定,也沒有任何人能關掉它。 它的規則很簡單:JavaScript 只能讀取同源的 response。
所謂「同源」,是指以下三個條件全部相同:
| 條件 | 範例 |
|---|---|
| Protocol | https:// vs http:// → 不同源 |
| Domain | app.example.com vs api.example.com → 不同源 |
| Port | localhost:3000 vs localhost:4000 → 不同源 |
只要三個裡面有任何一個不一樣,就是「跨域」,瀏覽器就會擋下 JavaScript 讀取 response。
Origin Header:整個機制的地基
瀏覽器怎麼知道請求從哪裡來?靠的是 request 裡的 Origin header。 這個 header 由瀏覽器自動帶上,而且是被鎖死的,JavaScript 無法覆蓋它。
你可能會想:「我自己在請求裡加一個 Origin: app.example.com 不就好了?」 試了也沒用。瀏覽器會靜靜地忽略你設的值,然後用真實的來源蓋掉。不是報錯,就是無聲地蓋掉。
整個 CORS 信任體系的地基,就是:後端信任瀏覽器帶來的 Origin,而瀏覽器保證這個值是真實的、無法偽造的。
CORS 機制拆解
Same-Origin Policy 是為了安全而設計的,但它也擋到了合法的場景。 你自己的前端 app.example.com 想打自己的後端 api.example.com,但瀏覽器不知道這是「自己人」,一樣擋。
CORS(Cross-Origin Resource Sharing) 就是解決這件事的機制。 它讓後端能告訴瀏覽器:「這個來源我允許,你可以放行。」
Same-Origin Policy 是鎖,CORS 是後端發給瀏覽器的鑰匙。
誰設定什麼?
| 機制 | 設定者 | 放在哪裡 |
|---|---|---|
| Same-Origin Policy | 瀏覽器內建,無人設定 | — |
Access-Control-Allow-Origin | 後端開發者 | Response header |
| CSRF Token | 後端產生、前端放入表單 | Request body / header |
這裡有一個很重要的細節:Access-Control-Allow-Origin 只有放在 response 裡才有意義。 瀏覽器是在收到 response 之後,才去檢查這個 header,然後決定要不要把內容給 JavaScript 讀。
有人會問:「那我在 request 裡加這個 header,可以讓自己的請求通過嗎?」 不行。後端看到了也沒有任何反應,因為這個 header 根本不是給後端看的。裁判是瀏覽器,而瀏覽器只在收到 response 的時候才去看它。
CORS 保護的邊界
這裡有個關鍵的細節,很多人沒意識到:
這個差別非常重要:
| 操作類型 | CORS 能保護嗎? | 原因 |
|---|---|---|
| GET 讀取個人資料 | ✅ 能保護 | JS 讀不到 response,資料拿不走 |
| POST/DELETE 觸發操作 | ⚠ 不一定 | 請求已送出、後端已執行,CORS 才檢查 |
這就是為什麼光靠 CORS 不夠,觸發操作類的請求,需要額外的機制來保護。 這就帶到了 Preflight 和 CSRF Token 的設計。
Preflight:送出去之前先問一聲
既然 CORS 擋不住「請求送出去」這件事,瀏覽器就設計了另一個機制: 對於有潛在危險的請求,在送出真正的請求之前,先送一個探路請求去問後端:「我待會要這樣打你,你允許嗎?」
這個探路請求就是 Preflight,使用 OPTIONS method。
哪些請求會觸發 Preflight?
瀏覽器把請求分成兩類:簡單請求(Simple Request)和非簡單請求。 判斷標準只有一個原則:
這個請求,是 HTML<form>或<a>標籤本來就能發的嗎?
是 → 不需要 Preflight。不是 → 先問。
具體條件,需要同時滿足以下全部,才算「簡單請求」:
| 條件 | 允許的值 |
|---|---|
| Method | GET / POST / HEAD |
| Content-Type | application/x-www-form-urlencoded / multipart/form-data / text/plain |
| Headers | 只有瀏覽器原生帶的 header,沒有任何自訂 header |
只要有任何一個條件不符合,就會觸發 Preflight。在現代開發中,這幾乎是必然:
- 幾乎所有 API 都用
application/json(不在允許列表) - JWT 認證需要
Authorization: Bearer xxx(自訂 header) - RESTful API 常用
PUT/DELETE(不在允許 method 內)
Preflight 的效能問題
每個非簡單請求都要先發一次 OPTIONS,等回應,才能發真正的請求。 對高頻 API 來說,這個額外的 round-trip 是有成本的。
解法是後端在 Preflight response 裡加上 Access-Control-Max-Age header, 告訴瀏覽器這個 Preflight 的結果可以快取多久。 在這段時間內,對同一個 endpoint 的相同類型請求不需要再發 Preflight。
# 告訴瀏覽器快取這個 Preflight 結果 1 小時
Access-Control-Max-Age: 3600CSRF Token:簡單請求的最後一道防線
Preflight 解決了非簡單請求的問題:在請求送出去之前先問。 但簡單請求不會觸發 Preflight,代表它們一定會送出去,後端一定會執行。
這裡有個微妙的地方:就算是簡單請求,evil.com 的 JavaScript 也讀不到 response(因為 CORS)。 但如果這個請求會觸發副作用,比如一個 POST 表單提交,後端已經執行了,不管 JavaScript 看不看得到 response。
所以對於會修改狀態的簡單請求,後端需要一個額外的驗證機制。這就是 CSRF Token。
運作原理
使用者載入頁面時,後端產生一個隨機的、只有這個 session 才有的 token,藏在 HTML 表單裡
使用者提交表單時,token 跟著請求一起送到後端
後端比對 token 是否正確,不對就拒絕請求
問題是:evil.com 為什麼拿不到這個 token? 因為 token 藏在 app.example.com 的 HTML 裡, 而 evil.com 的 JavaScript 想讀取 app.example.com 的頁面,這是跨域請求,被 Same-Origin Policy 擋住了。
CSRF Token 的保護性,是寄生在 Same-Origin Policy 上面的。
Same-Origin Policy 保護了 token 不被偷,token 保護了簡單請求不被偽造。
你可能在 Laravel 或其他框架看過這個:
<!-- Laravel Blade 表單 -->
<form method="POST" action="/profile">
@csrf
<!-- 展開後是: -->
<!-- <input type="hidden" name="_token" value="abc123xyz..."> -->
...
</form>整條防護鏈
把以上所有機制放在一起,就能看到它們是怎麼互相撐著對方的:
這四層不是各自獨立的,它們環環相扣。 拿掉任何一層,整個體系就會出現漏洞。 理解每一層在做什麼,才能在出問題的時候知道從哪裡下手。
2026 的架構思維:讓問題消失
好,那麼回到一開始的問題:2026 年的今天,跨域問題有什麼「新東西」嗎?
機制沒有新東西。Same-Origin Policy、CORS header、Preflight,這些在十幾年前就底定了,規範一行都沒改。
真正改變的是:現代架構已經讓「需要在瀏覽器層解決跨域」這件事,變成一個設計 smell。
以前的解題方式
五年前,典型的解法是:
// NestJS:設定 CORS,讓前端可以打
app.enableCors({
origin: 'https://app.example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true,
});這樣設沒有問題,今天也仍然是正確做法,如果你的架構需要前端直接打後端的話。 但問題是,大部分時候這個「如果」可以被架構設計消除掉。
Server Components:跨域在 server 端消失
還記得我們說的:跨域問題只存在於瀏覽器環境,因為瀏覽器有裁判。 如果請求根本不是從瀏覽器發出去的呢?
Next.js 的 Server Components 就是這個概念。 打 API 的程式碼在 server 端執行,瀏覽器只負責渲染結果。 Server 打 server,沒有瀏覽器,沒有裁判,跨域問題根本不存在。
// Server Component — 這段在 server 跑,不是瀏覽器
async function UserProfile() {
// 直接打外部 API,不會有 CORS 問題
// 因為這不是在瀏覽器裡執行
const data = await fetch('https://api.external-service.com/user');
const user = await data.json();
return <div>{user.name}</div>;
}Edge Middleware:瀏覽器以為在同源
另一個更通用的解法是 Edge Middleware(Cloudflare Workers、Next.js Middleware、Vercel Edge Functions)。
原理很簡單:在瀏覽器和後端之間放一個中間人,幫瀏覽器代理請求。 瀏覽器發請求到 app.example.com/api/xxx,中間人偷偷轉發到 api.external.com/xxx,再把 response 帶回來。 瀏覽器全程以為自己在打同源,CORS 規則根本不會觸發。
BFF(Backend for Frontend)
更傳統但同樣有效的做法是 BFF(Backend for Frontend)模式: 前端只打自己的後端(同源),由後端去打其他所有 API。 瀏覽器永遠在同源環境,跨域問題集中在 server-to-server,根本不經過瀏覽器裁判。
2026 新增事項:Private Network Access
有一個規範在這幾年落地,值得特別注意:Private Network Access(PNA)。
Chrome 從 2022 年開始逐步強制執行:公網頁面(https://app.example.com) 想訪問私有網路(localhost、192.168.x.x、內網 IP),需要後端回傳一個額外的 header:
Access-Control-Allow-Private-Network: true這對在做 homelab + 雲端前端的開發者影響很大。 你的本機 localhost:3000 如果要被一個 HTTPS 頁面存取,就需要特別處理這個 header。
AI Agent 帶來的新型跨域場景
2026 年最值得關注的不是規範本身,而是 AI Agent 架構帶來的新使用情境。
傳統跨域問題是:前端 → 後端。 Agent 的問題是:Agent 在後端動態調用各種工具和第三方 API, 但 OAuth callback、webhook 回調、SSE 串流、MCP Server 的 endpoint, 這些都需要仔細設計跨域策略。
本質上跨域規則沒變,但場景的複雜度大幅提升。 Agent 可能同時對接幾十個不同 origin 的服務,每個都有自己的 CORS 設定, 錯一個就會在某個特定的工具調用場景出問題。
結論
2026 年回頭看跨域,最大的感想不是「學了什麼新東西」, 而是「原來以前很多人包括我自己,根本沒搞清楚這件事在保護什麼」。
核心觀念總整理
- 跨域是瀏覽器的規則,curl、Postman、後端服務完全不受影響
- Same-Origin Policy 是鎖,保護 JS 不能讀跨域的 response
- CORS header 是鑰匙,後端在 response 裡設,告訴瀏覽器允許哪些 origin
- Preflight 是探路,非簡單請求在送出前先問後端,防止危險操作直接執行
- CSRF Token 補簡單請求的洞,靠 Same-Origin Policy 保護 token 不被偷
- 現代架構讓問題消失:Server Components、Edge Middleware、BFF 讓跨域根本不落到瀏覽器層
規則一行都沒改。但懂架構的人,早已感受不到它的存在。
如果你正在設計一個新的系統,發現自己在研究要怎麼設 CORS header, 不妨先退一步問:這個跨域請求能不能在架構層就消滅掉?很多時候答案是可以的。
本文最後更新於 2026-06-10。如果你發現任何錯誤或有補充,歡迎寄信跟我討論。
