跨域請求 CORS 解說,2026 回顧與更新

Security · Web Platform · 2026

跨域請求 CORS 解說,2026 回顧與更新

Same-Origin Policy 從 1995 年就存在,CORS 規範也在十幾年前底定。2026 再來回顧,規則一行都沒改,但懂架構的人,早已感受不到它的存在。

約 5,500 字CORSSecurityArchitectureWeb Platform

為什麼「跨域」是個問題?

在開始講 CORS 之前,先退一步問一個更根本的問題:為什麼跨域會是問題?這個問題的答案,決定了你對整個機制的理解深度。

跨域問題的根源,在於瀏覽器有一個其他工具沒有的特性:它會自動帶上 Cookie

想像這個情境:你登入了 facebook.com,瀏覽器幫你存了一個 session cookie。 接著你在另一個分頁開了一個陌生網站 evil.com。 這個頁面裡有一段 JavaScript,它偷偷發了一個請求到 facebook.com/api/messages

問題來了:瀏覽器會自動把你的 Facebook Cookie 帶上去。 如果沒有任何限制,evil.com 的 JavaScript 就能用你的身份讀取你的私訊、甚至刪除你的帳號,完全不需要知道你的帳號密碼。

核心問題
瀏覽器自動帶 Cookie 的機制,讓任何網站都有機會冒用你的已登入身份,對其他網站發出授權請求。

這就是 CSRF(Cross-Site Request Forgery,跨站請求偽造) 攻擊的原型。 而瀏覽器為了防範這件事,內建了 Same-Origin Policy(同源政策)

注意這裡說的是「瀏覽器」。curl、Postman、Node.js、你的後端服務,這些工具不會自動帶 Cookie、 沒有登入狀態的概念、也不存在「使用者正在瀏覽其他網站」這種情境。 所以它們根本不需要 Same-Origin Policy,跨域問題從一開始就只存在於瀏覽器環境

關鍵理解
「為什麼 Postman 可以打通,但前端一打就 CORS error?」因為 Postman 沒有瀏覽器的裁判機制。問題不在 API,在後端沒設好 response header。

Same-Origin Policy:瀏覽器的裁判規則

Same-Origin Policy 是瀏覽器廠商(Chrome、Firefox、Safari)出廠就內建的安全規則, 不需要任何人設定,也沒有任何人能關掉它。 它的規則很簡單:JavaScript 只能讀取同源的 response

所謂「同源」,是指以下三個條件全部相同:

條件範例
Protocolhttps:// vs http:// → 不同源
Domainapp.example.com vs api.example.com → 不同源
Portlocalhost:3000 vs localhost:4000 → 不同源

只要三個裡面有任何一個不一樣,就是「跨域」,瀏覽器就會擋下 JavaScript 讀取 response。

Origin Header:整個機制的地基

瀏覽器怎麼知道請求從哪裡來?靠的是 request 裡的 Origin header。 這個 header 由瀏覽器自動帶上,而且是被鎖死的,JavaScript 無法覆蓋它

你可能會想:「我自己在請求裡加一個 Origin: app.example.com 不就好了?」 試了也沒用。瀏覽器會靜靜地忽略你設的值,然後用真實的來源蓋掉。不是報錯,就是無聲地蓋掉。

為什麼要無聲蓋掉?
如果是報錯,惡意程式可以 catch 錯誤後繞過。無聲忽略讓攻擊者以為設成功了,但實際上沒有,這是刻意的設計。

整個 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 擋的是「JavaScript 能不能讀到 response」,不是「請求能不能送出去」。

這個差別非常重要:

操作類型CORS 能保護嗎?原因
GET 讀取個人資料✅ 能保護JS 讀不到 response,資料拿不走
POST/DELETE 觸發操作⚠ 不一定請求已送出、後端已執行,CORS 才檢查

這就是為什麼光靠 CORS 不夠,觸發操作類的請求,需要額外的機制來保護。 這就帶到了 Preflight 和 CSRF Token 的設計。

Preflight:送出去之前先問一聲

既然 CORS 擋不住「請求送出去」這件事,瀏覽器就設計了另一個機制: 對於有潛在危險的請求,在送出真正的請求之前,先送一個探路請求去問後端:「我待會要這樣打你,你允許嗎?」

這個探路請求就是 Preflight,使用 OPTIONS method。

BrowserServerOPTIONS /api/dataOrigin: app.example.com | Access-Control-Request-Method: DELETE200 OK — 允許Access-Control-Allow-Origin: app.example.com | Allow-Methods: DELETE✓ Preflight 通過,送出真正的請求DELETE /api/data200 OK — 資料回傳Access-Control-Allow-Origin: app.example.com✓ 瀏覽器確認,JavaScript 可以讀取

哪些請求會觸發 Preflight?

瀏覽器把請求分成兩類:簡單請求(Simple Request)非簡單請求。 判斷標準只有一個原則:

這個請求,是 HTML <form> <a> 標籤本來就能發的嗎?
是 → 不需要 Preflight。不是 → 先問。

具體條件,需要同時滿足以下全部,才算「簡單請求」:

條件允許的值
MethodGET / POST / HEAD
Content-Typeapplication/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。這不是問題,是正常的。真正的問題是不知道它在做什麼,然後把它當成 bug 去處理。

Preflight 的效能問題

每個非簡單請求都要先發一次 OPTIONS,等回應,才能發真正的請求。 對高頻 API 來說,這個額外的 round-trip 是有成本的。

解法是後端在 Preflight response 裡加上 Access-Control-Max-Age header, 告訴瀏覽器這個 Preflight 的結果可以快取多久。 在這段時間內,對同一個 endpoint 的相同類型請求不需要再發 Preflight。

# 告訴瀏覽器快取這個 Preflight 結果 1 小時
Access-Control-Max-Age: 3600

CSRF Token:簡單請求的最後一道防線

Preflight 解決了非簡單請求的問題:在請求送出去之前先問。 但簡單請求不會觸發 Preflight,代表它們一定會送出去,後端一定會執行。

這裡有個微妙的地方:就算是簡單請求,evil.com 的 JavaScript 也讀不到 response(因為 CORS)。 但如果這個請求會觸發副作用,比如一個 POST 表單提交,後端已經執行了,不管 JavaScript 看不看得到 response。

所以對於會修改狀態的簡單請求,後端需要一個額外的驗證機制。這就是 CSRF Token

運作原理

01
後端產生 token

使用者載入頁面時,後端產生一個隨機的、只有這個 session 才有的 token,藏在 HTML 表單裡

02
前端帶著 token 提交

使用者提交表單時,token 跟著請求一起送到後端

03
後端驗證 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>

整條防護鏈

把以上所有機制放在一起,就能看到它們是怎麼互相撐著對方的:

Same-Origin Policy瀏覽器內建 · 無法關閉 · 保護「JS 讀不到跨域 response」→ evil.com 讀不到你的頁面 HTML、讀不到你的 API response、偷不到 CSRF tokenCORS Header後端設定 · Access-Control-Allow-Origin · 合法跨域的白名單→ 告訴瀏覽器哪些 origin 可以讀 response,其他一律擋Preflight(OPTIONS)瀏覽器自動觸發 · 非簡單請求才有 · 在送出前先確認後端允許→ DELETE / PUT / 自訂 header / application/json → 先問,不讓危險請求直接送出CSRF Token開發者實作 · 彌補簡單請求沒有 Preflight 的空缺→ 後端驗 token,拿不到 token 的請求(evil.com)一律拒絕

這四層不是各自獨立的,它們環環相扣。 拿掉任何一層,整個體系就會出現漏洞。 理解每一層在做什麼,才能在出問題的時候知道從哪裡下手。

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 規則根本不會觸發。

(瀏覽器認為同源)app.example.com/api/xxx(Middleware 代理)api.external.com/xxxBrowserEdge MiddlewareExternal API✓ 瀏覽器以為同源,無 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 設定, 錯一個就會在某個特定的工具調用場景出問題。

n8n / Agent 開發者注意
n8n workflow、後端打後端的 HTTP Request node,這些完全不在瀏覽器環境,不受 CORS 限制。但如果你的 n8n 前端 UI 本身要打某個 API,那又回到瀏覽器規則了。分清楚「誰在打這個請求」是關鍵。

結論

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。如果你發現任何錯誤或有補充,歡迎寄信跟我討論。