為什麼還要再寫一篇 CORS#

基於安全考量,瀏覽器以同源政策(Same-Origin Policy)限制網頁存取其他 Origin 的資源(AJAX、DOM、Cookie、圖片等)。CORS (Cross-Origin Resource Sharing) 則提供了一條合法管道,讓符合條件的請求能合法跨過同源政策。

本章從 debug 視角彙整流程與排錯重點。從前端工程整體脈絡的整理,可參考 01-frontend/05-frontend-engineering/07-cors,兩篇互補閱讀。

Cross-Origin 與錯誤訊息#

只要兩個網址的 Scheme、Host、Port 任一不同,就算 Cross-Origin。當瀏覽器擋下跨域回應時,Console 會顯示類似這段訊息:

Access to fetch at [url] from origin [origin] has been blocked by CORS policy: 理由

值得注意的是:請求其實已經送出、伺服器也回了 Response,只是因為違反 CORS 規則,瀏覽器不允許 JavaScript 讀取內容。

啟動 CORS 的兩種模式#

CORS 分為簡單請求(Simple Request)與預檢請求(Preflight Request)兩種,是否成功多半取決於後端設定。前端能做的,往往只是看著錯誤訊息提醒後端調整。

共通的首要條件#

瀏覽器會在請求中自動加上 Origin Header,後端必須回傳對應的 Access-Control-Allow-Origin

Access-Control-Allow-Origin: *                   # 任意來源都接受
Access-Control-Allow-Origin: http://example.com  # 僅允許特定 Origin

簡單請求#

當 Method 為 GETHEADPOST 三者之一,且 Content-Type 屬於以下其中一種時,視為簡單請求,後端只要回對 Access-Control-Allow-Origin 即可:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

完整規則請參考 MDN 的 Simple requests 章節。

預檢請求(Preflight Request)#

只要不滿足簡單請求條件——例如使用 PUTDELETE,或 Content-Typeapplication/json——瀏覽器在送出實際請求前,會先發一次預檢。預檢沒過,就根本不會送出真正的請求。

預檢時,瀏覽器以 OPTIONS 先打招呼:

OPTIONS /data HTTP/1.1
Origin: http://example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header

後端據此決定要開放哪些設定:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: PUT, POST, GET, DELETE, OPTIONS
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 86400

只要請求宣告的 OriginAccess-Control-Request-* 都在允許清單內,瀏覽器才會送出正式請求;正式 Response 仍須回對應的 Headers,CORS 才算完成。

Access-Control-Max-Age 則告訴瀏覽器這份預檢結果可快取多久(單位為秒),上例的 86400 表示一天內不再重新預檢。

預設情況下,CORS 請求是匿名(Anonymous)發送,Cookie 不會被帶上、Response 的 Set-Cookie 也不會生效。要走「跨域帶身份」流程,前後端都要做設定。

前端:宣告需要 Credentials#

以 Fetch 為例,無論是簡單還是預檢請求,加上 credentials: 'include' 後,瀏覽器送出時就會附帶 Cookie,Response 的 Set-Cookie 也才有效:

fetch("https://example.com", { credentials: "include" });

後端:補上 Allow-Credentials 並收緊 Origin#

後端除了原本的 CORS 設定,還必須回 Access-Control-Allow-Credentials: true,否則瀏覽器收到 Response 會直接忽略。同時,Access-Control-Allow-Origin 不能再是萬用字元 *,必須與請求的 Origin 完全一致:

Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true

簡單請求情境下,正式 Response 對了就一切順利;預檢情境下則只有正式 Response 的 Set-Cookie 會生效。

Cross-Origin 不等於 Cross-Site#

把 Credentials 都打開,僅僅是「讓瀏覽器知道你需要帶 Cookie」。最終是否真的送出,仍要看 Cookie 的 SameSite 屬性。判定 Same-Site 的細節可參考 01-samesite

當 Cookie 的 SameSiteStrictLax

  • Cross-Origin 但 Same-Site:補上 Credentials 設定即可送出 Cookie。
  • Cross-Origin 又 Cross-Site:即使設了 Credentials,也會被 Same-Site 規則擋下;要送出,必須將 Cookie 標記為 SameSite=None; Secure

Cross-Site Cookie 也就是俗稱的第三方 Cookie(Third-Party Cookie)。

crossOrigin 屬性#

開發時可能遇過字體載不了,或想把跨域圖片畫到 Canvas 上時被擋下並出現 The canvas has been tainted by cross-origin data 錯誤——這些錯誤都和 CORS 相關。

要讓這類資源走 CORS 通道讀取,必須把 crossOrigin 屬性設為 anonymous。這同時告訴後端「不必回傳 Credentials」、告訴瀏覽器「這份資源沒有隱私問題」,瀏覽器才會放心讓 JavaScript 進一步處理:

img.crossOrigin = "anonymous";

字體預載的對應寫法可參考 W3C 的 Font fetching requirements

<link
  rel="preload"
  href="awesome-font.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10251693