具代表性的面試主題

前端面試:如何排查 CORS 預檢與憑證請求?

前端中等
OfferCC 編輯團隊發佈 更新

題幹

頁面 https://app.example.com 使用 fetch 向 https://api.example.com/profile/preferences 傳送 JSON POST,並攜帶 X-CSRF-Token 與登入 Cookie。請求在 curl 中成功,在瀏覽器中卻回報 CORS 錯誤。請解釋瀏覽器會傳送什麼請求、定位錯誤設定、給出安全修正,並說明如何驗證。

題目與適用情境

頁面執行於 https://app.example.com,API 位於 https://api.example.com。前端需要提交使用者偏好:

ts
await fetch("https://api.example.com/profile/preferences", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
    "X-CSRF-Token": csrfToken,
  },
  body: JSON.stringify({ compactMode: true }),
})

伺服器目前對 OPTIONS 回傳:

http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST

curl 直接呼叫 POST 可以成功,瀏覽器主控台卻只顯示跨來源錯誤。請說明:

  1. 為什麼這兩個子網域仍是跨來源請求;
  2. 瀏覽器為什麼先傳送 OPTIONS,什麼情況下才會繼續傳送 POST;
  3. 目前的回應標頭有哪些問題,應該如何依最小權限修正;
  4. CORS、Cookie、驗證、授權和 CSRF 分別負責什麼;
  5. 如何在開發者工具與自動化測試中證明修正有效。

這道題適用於前端與全端職缺。它考察的不是背一組回應標頭,而是能否從瀏覽器網路階段反推 故障位置,並把「請求是否傳送」「憑證是否攜帶」「指令碼能否讀取回應」「業務是否允許操作」 四件事拆開。

面試官考察重點

首先要準確判斷來源。Origin 由通訊協定、主機和連接埠組成。兩個位址雖然同為 HTTPS,且通常 屬於同一站點,但主機不同,因此是同站跨來源請求。CORS 依來源判斷;Cookie 的 SameSite 依站點判斷,兩者不能混用。

其次要理解預檢條件。application/json 不屬於 CORS-safelisted 請求標頭值, X-CSRF-Token 也不是 safelisted 請求標頭,因此瀏覽器先傳送 OPTIONS,詢問伺服器是否允許 實際方法與請求標頭。預檢失敗時,實際 POST 不會傳送。

第三,要能讀出目前設定的兩個關鍵錯誤:帶憑證的回應不能使用 Access-Control-Allow-Origin: *;預檢也沒有允許 Content-Type 與 X-CSRF-Token。允許方法中出現 POST 還不夠。

最後要守住安全邊界。CORS 決定瀏覽器是否把跨來源回應共享給指令碼,不取代驗證、授權或 CSRF 防護。伺服器不能為了消除主控台錯誤就反射任意 Origin,也不應把所有方法與請求標頭 全部放開。

回答前需要釐清的問題

  • 失敗發生在哪個網路階段? Network 面板裡完全沒有請求、只有 OPTIONS、已有 POST 但

JavaScript 讀不到回應,代表不同故障。

  • 請求是否必須帶 Cookie? 若使用 Bearer token 或匿名公開資源,憑證規則與允許來源策略會

不同。

  • 允許哪些精確的前端 Origin? 需要列出生產、預備和本機開發來源,不能只說「允許公司

網域」。通訊協定與連接埠也必須精確。

  • OPTIONS 是否被閘道、驗證中介軟體或重新導向攔截? 預檢通常不帶使用者憑證,不能先

要求它通過登入狀態驗證。

  • Cookie 的 Domain、Path、Secure 與 SameSite 是什麼? CORS 通過也不保證瀏覽器會傳送

Cookie。

  • 實際回應與錯誤回應是否都帶 CORS 標頭? 若 401、403 或 500 遺漏允許來源標頭,瀏覽器

可能用泛化的 CORS 錯誤遮住真正狀態。

  • 是否存在 CDN 或共享快取? 如果回應會隨 Origin 動態變化,應傳送 Vary: Origin,避免

一個來源的回應被錯誤重用給另一個來源。

30 秒回答框架

「我會把問題拆成四道關卡:來源判斷、預檢許可、憑證傳輸和回應共享。兩個子網域主機不同, 所以是跨來源。JSON Content-Type 和 X-CSRF-Token 會觸發 OPTIONS;瀏覽器只有看到精確允許的 Origin、POST 和兩個請求標頭,才會傳送實際請求。這裡使用 credentials: include,所以 Allow-Origin 不能是星號,應該在嚴格允許清單命中後回傳 https://app.example.com,同時回傳 Allow-Credentials: true 和 Vary: Origin。OPTIONS 本身不依賴登入 Cookie;真正的 POST 仍要做驗證、授權與 CSRF 檢查。最後我會在 Network 面板逐個核對 OPTIONS、POST、Cookie、回應標頭與業務狀態,並用允許來源與惡意來源的矩陣 驗證。」

分步深入解答

第一步:先畫出瀏覽器實際執行的狀態機。

預檢請求大致如下:

http
OPTIONS /profile/preferences HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-csrf-token

瀏覽器比較的不是「前後端是不是同一家公司」,而是回應是否明確許可這個 Origin、方法和 請求標頭。可以用下表快速定位:

Network 證據說明下一步
沒有 OPTIONS 或 POSTJavaScript 尚未執行、URL 被 CSP 等更早規則攔截先檢查呼叫與主控台
只有失敗的 OPTIONS實際 POST 未傳送檢查預檢狀態碼、重新導向和允許標頭
OPTIONS 成功,POST 無 CookieCORS 許可已通過,憑證鏈路有問題檢查 credentials 與 Cookie 屬性
POST 回傳 401/403/500,但指令碼只見 CORS錯誤回應缺少有效 CORS 標頭保留真實狀態並補齊允許來源標頭
POST 成功且指令碼可讀CORS 鏈路通過再驗證業務狀態與安全負例

第二步:依請求推導最小允許集合。

對於題目中唯一允許的生產前端,可以回傳:

http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token
Vary: Origin

伺服器應先把 Origin 與精確允許清單比較,命中後才寫回該值。不要把用戶端傳來的 Origin 無條件複製到回應。若還有預備或本機環境,應把每個完整 Origin 分別列入相應環境設定, 不要用字串後綴比對來接受偽造的相似網域。

公開、匿名且不帶憑證的資源有時可以使用 *。這個使用者偏好介面帶 Cookie,屬於敏感操作, 必須回傳具體 Origin。預檢 OPTIONS 不攜帶 Cookie;閘道可以在驗證前處理預檢,但仍要嚴格 檢查 Origin、方法與請求標頭。

第三步:再檢查實際請求與憑證。

預檢成功後,瀏覽器才會傳送 POST。credentials: "include" 允許跨來源請求進入憑證處理, 但 Cookie 是否真正附帶仍受 Domain、Path、Secure、SameSite 與瀏覽器隱私策略約束。

本例兩個 HTTPS 子網域通常是同站跨來源:SameSite 規則可能允許 Cookie,CORS 仍然適用。 不能從「CORS 已通過」推導「Cookie 一定傳送」,也不能從「Cookie 已傳送」推導「指令碼可以 讀取回應」。應在 Network 面板分別檢查 Request Cookies、回應狀態與 CORS 回應標頭。

第四步:把 CORS 與業務安全拆開。

CORS 是瀏覽器的回應共享協定,不是伺服器存取控制。curl 和伺服器端 HTTP 用戶端不會替 瀏覽器執行同源策略,所以 curl 成功只能證明 API 能處理請求,不能證明瀏覽器鏈路正確。

實際 POST 仍需:

  • 驗證登入身分;
  • 驗證目前使用者是否有權修改目標資源;
  • 驗證 CSRF token 或採用同等強度的防護;
  • 檢查輸入並維持冪等或重複提交策略;
  • 對允許來源的 401、403 和 500 回應同樣回傳正確 CORS 標頭,讓前端看見真實錯誤。

「惡意頁面讀不到回應」不等於「請求沒有副作用」。某些不需要預檢的跨來源表單式請求可能 已經到達伺服器,因此敏感寫入不能只靠 CORS 防 CSRF。

第五步:用正負測試證明邊界。

修正後至少驗證:

測試案例預期結果
允許來源傳送題目請求OPTIONS 204,隨後 POST;指令碼讀到真實回應
允許來源缺少 CSRF token依協定設定決定是否預檢;實際寫入必須被業務檢查拒絕
未允許來源傳送相同請求不回傳允許該來源的 CORS 標頭,實際預檢不通過
請求未允許的方法或請求標頭預檢失敗,POST 不傳送,狀態不變
Cookie 過期POST 回傳真實 401,前端可讀取並進入登入流程
伺服器回傳 500前端看到 500 與結構化錯誤,而非泛化 CORS 提示
連續相同請求命中預檢快取行為仍正確;設定變更時重新驗證冷快取

同時核對伺服器日誌:失敗的預檢不應出現對應 POST,也不應產生使用者偏好更新。日誌可以 記錄 Origin、方法、請求標頭集合和拒絕原因,不應記錄 Cookie 或 CSRF token 值。

高品質示範回答

「我先判斷請求所處階段。app.example.com 和 api.example.com 的主機不同,所以即使它們 同站,仍然跨來源。這個 POST 使用 JSON Content-Type 和 X-CSRF-Token,瀏覽器會先發一個 不帶 Cookie 的 OPTIONS,其中宣告 Origin、POST 和兩個非簡單請求標頭。只有預檢回應允許 這三項,瀏覽器才繼續傳送 POST。

目前設定有兩個直接問題。第一,前端設定了 credentials: include,帶憑證回應不能把 Access-Control-Allow-Origin 設為星號;第二,回應沒有 Access-Control-Allow-Headers: Content-Type, X-CSRF-Token。我會在伺服器用精確允許清單 檢查 Origin,命中 https://app.example.com 後回傳這個具體值、Allow-Credentials、POST、 兩個允許請求標頭以及 Vary: Origin。OPTIONS 放在使用者驗證中介軟體之前處理,但未允許的 Origin、方法或請求標頭仍會被拒絕。

預檢通過後,我再檢查 POST 的 Request Cookies。credentials: include 只是必要條件,Cookie 仍受 Domain、Path、Secure、SameSite 和瀏覽器策略約束。真正的 POST 必須繼續做驗證、授權 與 CSRF 檢查,因為 CORS 只控制瀏覽器是否把回應共享給指令碼,不是存取控制。

驗證時我會在 Network 面板確認 OPTIONS 成功後才出現 POST,檢查請求 Cookie、實際狀態與 回應標頭;再用允許來源、未允許來源、未允許方法、過期登入狀態和 500 回應做正負測試。 伺服器日誌還要證明預檢失敗時沒有 POST,也沒有狀態變化。curl 成功只能作為 API 基線, 不能取代瀏覽器 CORS 驗證。」

常見錯誤

  • 把同站當成同源 → 子網域不同仍會觸發 CORS → 依通訊協定、主機、連接埠逐項比較。
  • 看到 POST 在允許清單就認為預檢會過 → 非簡單請求標頭未被允許 → **同時核對 Origin、

Method 和 Headers。**

  • 帶憑證時使用 Allow-Origin: * → 瀏覽器拒絕把回應暴露給指令碼 → **回傳允許清單

命中的具體 Origin。**

  • 無條件回顯請求 Origin → 任意網站都可能獲得帶憑證回應的讀取權限 → **先做精確允許

清單檢查。**

  • 要求 OPTIONS 先通過使用者登入 → 預檢通常不攜帶憑證 → **在驗證前完成受限的預檢

處理。**

  • 用 mode: "no-cors" 修正 → 前端拿到的是不可讀的 opaque response → **修正伺服器

CORS 協定。**

  • 把 CORS 當作 CSRF 防護 → 請求可能到達伺服器並產生副作用 → **敏感寫入繼續檢查

CSRF 與授權。**

  • 只給 2xx 加 CORS 標頭 → 401 或 500 被偽裝成跨來源錯誤 → **允許來源的錯誤回應也

回傳一致 CORS 標頭。**

  • 只用 curl 驗證 → curl 不執行瀏覽器同源策略 → 必須用真實瀏覽器網路證據驗收。

追問與應對

為什麼請求有時不發 OPTIONS,卻仍然回報 CORS?

符合 safelisted 方法、請求標頭與 Content-Type 的請求可以直接傳送。瀏覽器仍會檢查實際回應 的 CORS 標頭;不符合共享條件時,JavaScript 讀不到回應。此時伺服器可能已經處理請求,所以 要從 Network 面板確認「請求已傳送但回應未共享」,不能把所有 CORS 錯誤都稱為預檢失敗。

為什麼 Access-Control-Allow-Credentials 不應該出現在預檢請求裡?

它是伺服器的回應標頭,表示瀏覽器可以向指令碼暴露憑證模式下的實際回應。用戶端透過 Fetch 的 credentials mode 表達憑證意圖;預檢請求本身不帶使用者憑證。實際回應也要符合具體允許 來源和 Allow-Credentials 條件。

Vary: Origin 解決什麼問題?

當伺服器依請求 Origin 回傳不同的 Access-Control-Allow-Origin 時,共享快取需要把 Origin 納入快取鍵。否則,一個允許來源的回應可能被重用給另一個來源,造成錯誤放行或錯誤攔截。

CORS 已正確,為什麼 Cookie 仍然沒有傳送?

繼續檢查前端是否設定 credentials: "include",以及 Cookie 的 Domain、Path、Secure、 SameSite、到期時間與瀏覽器隱私限制。CORS 與 Cookie 傳輸是相鄰但獨立的關卡。

如何處理多個合法前端網域?

維護精確允許清單,請求到達時檢查完整 Origin,命中後回寫該 Origin 並傳送 Vary: Origin。 不要回傳多個 Allow-Origin 值,也不要用寬鬆正規表示式、後綴比對或無條件反射。

前端能否自行修正伺服器 CORS?

不能。前端可以減少不必要的非簡單請求標頭,或依協定設定 credentials,但允許跨來源共享的 回應標頭必須由 API、反向代理或閘道正確回傳。生產程式碼不應依賴瀏覽器外掛或關閉安全策略。

應該把預檢快取多久?

根據設定變更頻率、撤銷需求和瀏覽器上限選擇,並在發佈 CORS 變更時驗證冷快取與既有快取 兩條路徑。快取能減少 OPTIONS 流量,但會延長錯誤許可的生效時間,不能用來掩蓋設定問題。

公開來源

同類題目