當在單頁應用程式(SPA)中處理 OAuth 時,存取權杖與更新權杖該存放在何處,一直是長期的爭論。localStorage、sessionStorage 以及記憶體變數都無法完全抵禦 XSS。Backend for Frontend(BFF)模式是一種將權杖保留在伺服器端、而非傳遞給瀏覽器的設計。本文將整理其機制與關鍵實作要點。
前置條件
本文假設以下環境:
- 使用 OAuth 2.0 與 OpenID Connect 開發瀏覽器型應用程式。
- 存在一個 SPA、它所呼叫的 API,以及一個授權伺服器。
- SPA 與其支援的伺服器端元件可置於相同父網域下。
- HTTPS 為前提條件(用於發行 Secure Cookie)。
瀏覽器型應用程式中的 XSS 風險
在瀏覽器中執行的應用程式程式碼,容易受到瀏覽器執行環境中一切行為的影響。XSS 的影響範圍廣泛,且由於攻擊程式碼在與應用程式相同的環境中執行,因此可能進行以下操作:
- 讀取儲存在 localStorage 或 sessionStorage 中的值。
- 讀取可從 JavaScript 存取的記憶體變數。
- 執行應用程式所能進行的所有 API 呼叫。
- 透過覆寫內建函式(原型污染)來改變行為。
XSS 有多種進入點,例如依賴函式庫的漏洞、自身程式碼輸入/輸出處理的缺陷,或第三方指令碼的入侵。由於完全防止 XSS 相當困難,務實的策略是「在發生入侵時限制其影響」。
只要權杖放在瀏覽器中,權杖經由 XSS 被竊取的可能性就始終存在。若攻擊者在自己的環境中使用竊取到的更新權杖,即使用戶關閉網站,他們也能長時間呼叫 API。雖然權杖輪換與閒置逾時可以減緩影響,但並非根本解決方案。
不將權杖放在瀏覽器中的設計
BFF 模式提供一個專屬於 SPA 的伺服器端元件,並將 OAuth 客戶端責任集中於此。SPA 不直接處理 OAuth 流程,而是透過 BFF 進行身分驗證與 API 呼叫。
角色劃分如下:
- 與授權伺服器的 OAuth 協定通訊:由 BFF 處理。
- 持有存取權杖與更新權杖:僅限 BFF。
- 維護 SPA 與 BFF 之間的身分驗證狀態:使用 HttpOnly Cookie。
- API 呼叫:SPA 將請求送至 BFF,BFF 將 Cookie 轉換為權杖再轉發給 API。
在此配置中,權杖僅在 BFF 與它所呼叫的 API 之間流轉,對瀏覽器的 JavaScript 不可見。即使發生 XSS,攻擊者也無法提取權杖並從其他地方使用。攻擊者所能做的僅是在用戶當前開啟的工作階段範圍內,向 BFF 發送請求。雖然此影響不可忽視,但其持續時間與影響範圍與權杖被竊相比,已大幅受限。
BFF 扮演 OAuth 術語中所謂的「機密客戶端」。它持有客戶端密碼,並完全在伺服器端完成授權碼交換權杖與更新處理。
身分驗證流程
典型的身分驗證流程如下:

- SPA 啟動登入時,向 BFF 發送登入請求。
- BFF 啟動 PKCE 授權碼流程,產生導向授權伺服器的重新導向 URL,並回傳給 SPA。
- SPA 將瀏覽器導向該 URL,用戶在授權伺服器進行身分驗證。
- 授權伺服器將授權碼回傳至 BFF 的重新導向 URI。
- BFF 用授權碼交換權杖,取得存取權杖與更新權杖。
- BFF 將權杖儲存在自己的安全區域(加密 Cookie、伺服器端工作階段儲存空間等),並僅透過 HttpOnly Cookie 將工作階段識別碼回傳給 SPA。
- SPA 呼叫 API 時,透過 BFF 發送請求。Cookie 會同時發送,BFF 將其轉換為存取權杖再轉發給上游 API。
- 若存取權杖過期,BFF 會使用更新權杖靜默更新。
從 SPA 的角度來看,登入狀態由 Cookie 維護,而 API 請求則以一般 fetch 呼叫完成。SPA 中不存在直接處理 OAuth 權杖或授權碼的程式碼。
必要的安全設定
BFF 發行的 Cookie 必須設定以下屬性:
- HttpOnly:禁止從 JavaScript 存取。即使有 XSS,也無法讀取 Cookie 內容。
- Secure:僅透過 HTTPS 發送。
- SameSite=Strict:確保 Cookie 不會隨其他網站的請求發送。這有助於阻擋主要的 CSRF 攻擊路徑,但 CSRF 防護並非僅靠此項即可完成。
- __Host- 前綴:在 Cookie 名稱前加上 __Host-,確保瀏覽器將 Cookie 限制在發行主機內,不會與子網域共用。
若將權杖儲存在加密 Cookie(客戶端工作階段)中,則 Cookie 內容會加密。若將權杖放在伺服器端工作階段儲存空間(伺服器端工作階段)中,則 Cookie 僅包含工作階段識別碼,因此不需加密。
CSRF 防護不應僅依賴 SameSite
由於採用 Cookie 基礎的身分驗證,BFF 必須實作 CSRF 防護。SameSite=Strict 是有效的一步,但並非完整解決方案。尤其當 SPA 放置於 www.example.com 而 BFF 放置於 api.example.com 時需特別注意。由於 SameSite 是依網站而非來源判定,來自 example.com 下其他子網域的請求會被視為「相同網站」,即使使用 SameSite=Strict,Cookie 仍會被發送。
因此,請使用以下其中一種方式加強 CSRF 防禦:
- 若 BFF 與 SPA 位於不同來源,則使用 CORS 與 Origin 標頭驗證進行防禦。
- 對於改變狀態的請求,使用防偽 / 雙重提交 Cookie 方法驗證 CSRF 權杖。
將 CORS 限制為確切來源
CORS 應僅允許 SPA 的確切來源。對於涉及 Cookie 的認證請求,瀏覽器不允許在 Access-Control-Allow-Origin 中使用萬用字元(*)。因此,BFF 必須在回應中反映 SPA 的確切來源。這種嚴格限制允許來源的做法,可作為 CSRF 防禦的一部分。
BFF 所持有的工作階段資料(尤其是儲存在加密 Cookie 中的權杖資訊)需要金鑰管理。請將安全注入金鑰作為 BFF 設定的一部分,並定期輪換金鑰。
請注意,將 SPA 與 BFF 置於相同父網域,是為了滿足 Same-Site Cookie 作為第一方 Cookie 運作的條件。
演進:API 驅動的 BFF
若 BFF 像傳統 Web 應用程式一樣建置,BFF 的伺服器端渲染會介入 SPA 的頁面切換。API 驅動的 BFF 能最小化此影響。
角色分為兩個:
- OAuth Agent:負責 OAuth 協定處理的 API。它透過 JSON 從 SPA 呼叫。
- OAuth Proxy:作為 API 閘道外掛運作,從 Cookie 中提取權杖,再轉發給上游 API。
在此配置中,SPA 僅需將 OAuth Agent 視為一般 REST API 呼叫。SPA 前端開發體驗幾乎可維持與導入 BFF 前相同。
採用時的考量
採用 BFF 模式時,請考慮以下幾點:
- 增加的架構元件會提高開發與運維成本。
- SPA 與 BFF 必須置於相同父網域。
- 僅透過反向代理轉發 Authorization 標頭的配置並非 BFF。BFF 的本質在於作為機密客戶端持有權杖。
- PKCE 與 BFF 是搭配使用,而非替代關係。
- BFF 在轉發前必須驗證目標主機,以防止權杖暴露給非預期的主機。
- 登出設計會變得複雜,因為必須同時連動 SPA 工作階段、BFF Cookie 與授權伺服器工作階段。
總結
在瀏覽器型應用程式中安全處理權杖的實務做法,就是不將權杖放在瀏覽器中。BFF 模式將 OAuth 客戶端責任移至伺服器端,僅將 HttpOnly Cookie 傳遞給瀏覽器。這樣即使發生 XSS,也能防止權杖被竊。透過將角色劃分為 OAuth Agent 與 OAuth Proxy,可在維持 SPA 開發體驗的同時強化安全性。





