Communitygithub.com

rbikbov/frontend-starter

Next.js (App Router) 前端中的授權與 Session 管理:Token 存放位置、基於 Route Handler 的 BFF 搭配 HttpOnly Cookie、無競爭條件的 Refresh Token、未授權判定(含回傳 200 的 GraphQL)、proxy.ts 與 Data Access Layer 的權限檢查、登入後的安全重新導向、不洩漏前一個使用者資料的登出、密碼重設連結中的 Token、以及登入與恢復表單。在設計或實作登入、註冊、密碼恢復、受保護頁面、含 Refresh Token 的 API 客戶端,或審查此類程式碼時使用。

frontend-starter 是什麼?

這個 AI Agent 技能提供在 Next.js (App Router) 前端中實作授權與 Session 管理的完整設計準則與實作規範。它詳細說明了多種 Session 模型的選擇(包括 BFF、第三方函式庫、Memory + HttpOnly Cookie 等),並嚴格規定必須使用 HttpOnly Secure SameSite Cookie 儲存 Token。技能涵蓋了 BFF Route Handler 的實作細節(輸入驗證、Origin 檢查、錯誤處理、Refresh 與 Logout 邏輯),以及在前端各層級(proxy.ts、Data Access Layer、客戶端)進行 Session 驗證的策略。特別強調客戶端 Refresh Token 的「單次飛行」機制以避免競爭條件,並詳細定義了未授權狀態的判定(HTTP 401 與 GraphQL UNAUTHENTICATED 錯誤碼)。此外,還包含了安全重新導向函式 getSafeRedirect 的設計、登出時清除客戶端快取與 Store 以防止資料外洩、從 URL 中安全移除一次性 Token、表單設計的最佳實踐、以及詳盡的測試清單與完成定義。此技能支援任何以 Next.js 為基礎的前端專案,並假設前端從後端 API 取得 Session。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/rbikbov/frontend-starter/tree/HEAD/.claude/skills/frontend-auth-session

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Next.js 中的授權與 Session 管理

此文件針對前端從後端取得 Session(Token 或 Session ID)後,需要儲存、延長與驗證 Session 的情境。如果專案使用函式庫或提供者(Auth.js、Better Auth、Clerk 等),Session 機制由它們處理,但第 2、6–10 節的規則仍然適用。程式碼範例請參閱 references/implementation.md。

規則優先順序。 使用者指令與專案的 agent 檔案權重更高。如果後端有不同要求(例如自行設定 Cookie),請遵循後端規範,並在 README 或 ADR 中記錄差異。

符號說明:MUST — 不得違反;SHOULD — 預設應遵守,偏離需有理由。


1. 模型選擇

模型適用時機
BFF: Next 的 Route Handler 從後端取得 Token,並放入 HttpOnly Cookie後端在回應主體中回傳 Token。Next 的預設選項
後端自行設定 HttpOnly Cookie前端與 API 位於相同網站,且後端受你控制
函式庫或提供者需要 OAuth、MFA、使用者管理 — 不要自行實作
Access Token 在記憶體,Refresh Token 在 HttpOnly Cookie無 BFF 的 SPA
Token 存放在 localStorage/sessionStorage禁止: 任何 XSS 都會導致 Session 被竊取

第二個選擇是 BFF 代理哪些請求:

選項優點缺點
僅代理 login / refresh / logout,其他請求直接帶 Cookie 發送給 APIBFF 不成為瓶頸;程式碼較少API 需從 Cookie 讀取 Token 並設定 CORS;Cookie 必須能送達 API 的網域
所有請求皆透過 BFFAPI 不需處理 Cookie 與 CORS;單一來源每個請求都多一次躍點;BFF 需要擴展

選擇結果需記錄在 ADR 中。


2. Cookie(MUST)

  • 所有能提供存取權限的 Token(access、refresh、session id)都必須設定 HttpOnly。
  • 正式環境必須設定 Secure。瀏覽器允許在本機 http://localhost 使用 Secure,但其他 http 主機則不行。
  • 預設使用 SameSite=Lax。Strict 對抗 CSRF 更安全,但使用者若透過郵件或其他網站的連結訪問,首次點擊時會顯示未登入狀態 — 請謹慎選擇並記錄原因。
  • 設定 Path=/;生命週期應根據 Token 的 exp 或後端回應來決定,不要「憑感覺」設定。
  • SHOULD:在 Cookie 名稱中加入 __Host- 前綴(需要 Secure、Path=/、無 Domain)— 可防止從子網域竄改 Cookie。
  • Cookie 只能在伺服器端(Route Handler、Server Action、proxy.ts)設定或刪除。Server Component 無法設定 Cookie。
  • 不可從 JavaScript 讀取的 Cookie 是 Session 的唯一載體。一個不帶 HttpOnly、僅作為 UI 提示用的 is_authenticated 標誌,絕對不能用於存取權限的判斷。

3. BFF:Route Handler(MUST)

  • 登入請求需經由 schema(zod)驗證;格式錯誤的回應 400。
  • 會修改狀態的處理器需檢查 Origin 是否與應用程式的 origin 一致:SameSite=Lax 無法防禦來自同網站子網域的請求。Server Actions 會自行比較 Origin 與 Host。
  • 需將客戶端的上下文(context)傳遞給後端:X-Forwarded-For、User-Agent — 否則後端的 Rate Limit 只會看到伺服器的 IP。
  • 後端的錯誤應轉換為包含 kind 與狀態碼(401、403、422、附帶 Retry-After 的 429、503)的回應,不得直接暴露後端的錯誤訊息。
  • Refresh: 如果後端拒絕 Refresh Token — 刪除 Cookie 並回應 401。如果後端無法連線(網路問題、5xx)— 不更動 Cookie,回應 503:暫時性錯誤不應導致使用者登出。
  • Logout: 在後端撤銷 Token(記錄錯誤但不顯示給使用者),無論如何都必須刪除 Cookie。

4. 檢查 Session 的位置(MUST)

位置功能不做什麼
proxy.ts(Next 16+,之前為 middleware.ts)樂觀檢查 Cookie 是否存在:將未登入者重新導向至登入頁面,並將已登入者導離登入頁面不查詢資料庫或 API;非唯一防護
Data Access Layer(server-only 模組中的 verifySession())在資料存取處進行驗證:Server Component、Route Handler、Server Action—
Layout不做任何事:Layout 在導覽時不會重新渲染不檢查存取權限
後端事實來源:在每個請求驗證 Token—
  • 每個 Route Handler 與 Server Action 都是公開的進入點,必須自行呼叫 verifySession()。
  • proxy.ts 應檢查 Refresh Cookie(或 Session ID):Access Token 可能已過期,但 Session 仍然有效。
  • 路徑比對應以區段為單位:/app 與 /app/… 匹配,但 /apple 不匹配。
  • 若專案啟用 cacheComponents(Next 16.4 預設),Session 讀取與使用者資料快取有各自的規則:在撰寫 DAL 之前,請先閱讀 node_modules/next/dist/docs/01-app/02-guides/authentication-with-cache-components.md。
  • 權限(角色、所有權)需根據驗證過的資料來決定:透過簽章(JWKS、jose)或後端回應。不可信賴未經驗證的 JWT payload 作為存取依據。
  • Server Component 無法更新 Cookie。如果在伺服器端渲染時 Access Token 已過期:要麼由 proxy.ts 更新 Session(它可以在回應中設定 Cookie — 僅對文件導覽(document navigation)這麼做,不要用於 prefetch,並需考慮 Refresh Token 輪換),要麼頁面渲染客戶端載入,由客戶端執行 Refresh。

5. 客戶端:無競爭條件的 Refresh(MUST)

  • 「未授權」的判定: HTTP 401,或者在 GraphQL 回應(狀態碼 200)中,至少有一個錯誤包含 extensions.code === "UNAUTHENTICATED"(使用 errors.some,而非 errors[0])。僅當後端不提供錯誤碼時,才比對訊息文字,且需寫在單一函式中,加上註解與測試。
  • 單次飛行(Single-flight): 所有同時收到 401 的請求應等待同一個 Refresh 請求,並使用其結果。Promise 應在 finally 中清除。
  • 原始請求只會重試一次。再次收到 401 表示 Session 已失效,不應再次嘗試 Refresh。
  • Refresh 請求本身不應經過攔截器(否則會造成遞迴)。
  • Refresh 的結果需區分三種情況:成功(true)→ 重試原始請求;被拒絕(false)→ Session 已過期;網路或 5xx 錯誤(例外)→ 回傳 network 錯誤,不登出帳號。
  • 在登出期間應封鎖 Refresh:否則,一個在登出瞬間收到 401 的請求,可能會立刻恢復 Session。
  • Session 過期時:清除請求快取,將使用者導向登入頁面,並附上當前頁面的 callbackUrl。
  • 要重試的請求主體必須是可重複讀取的(字串、FormData),不可使用一次性 ReadableStream。

6. 登入後的重新導向(MUST)

  • callbackUrl(next、returnTo)必須經由 getSafeRedirect(raw, fallback, appOrigin) 函式驗證:此函式應拒絕反斜線(\)與控制字元(瀏覽器會將 /\evil.com 與 /\<tab>/evil.com 轉換為 //evil.com),使用 new URL(raw, appOrigin) 進行解析,比較 origin,並僅回傳路徑部分。僅檢查「是否以 / 開頭且不是 //」是不夠的。
  • 此函式的完整測試案例請參閱 references/implementation.md 的第 7 節。
  • 登入後,Server Component 必須能看見新的 Cookie:應使用完整導覽(window.location.assign)或 router.replace 搭配 router.refresh()。

7. 登出(MUST)

  • 伺服器需刪除 Cookie(第 3 節)。
  • 客戶端需清除請求快取(queryClient.clear())以及所有儲存使用者資料的客戶端 Store:下一個使用者在同一個分頁中,不應短暫看到前一個使用者的資料。
  • 導覽至登入頁面時,不應允許「上一頁」回到已快取的受保護畫面(使用 replace 或完整導覽)。
  • 是否顯示登出確認訊息由產品決定;但請求進行期間必須鎖定按鈕。
  • SHOULD:其他分頁應能得知登出事件(透過 BroadcastChannel 或針對標誌的 storage 事件),否則它們會持續顯示資料,直到遇上第一個 401。

8. 連結中的 Token(密碼重設、Email 驗證)

  • Token 只能從 URL 讀取一次並存入狀態中,然後應立即替換掉含有 Token 的 URL(router.replace(pathname)):如此 Token 不會留在瀏覽歷史中,也不會透過 Referer 標頭傳送出去。
  • 這些頁面應設定 Referrer-Policy: no-referrer,且不應載入任何第三方腳本(分析工具、小工具)。
  • Token 不得被記錄。一次性與有效期限由後端負責;前端應顯示易懂的「連結已過期」畫面,並提供「重新發送」的操作。

9. 登入與恢復表單

  • 密碼恢復功能應對已註冊與未註冊的 Email 回傳相同的回應(「如果帳號存在,已寄送郵件」),不得洩漏哪些 Email 已註冊。如果後端會洩漏,請記錄此限制。
  • 請求進行時應鎖定送出按鈕;重複點擊不會建立第二個請求。
  • 429 狀態碼:如果回應中有 Retry-After,應顯示包含等待時間的訊息。
  • 欄位屬性:Email 使用 type="email" 搭配 autoComplete="email";密碼在登入時使用 autoComplete="current-password",在註冊與變更時使用 "new-password";不應封鎖貼上功能。
  • 「顯示密碼」按鈕:type="button",需有 aria-label 與 aria-pressed 屬性,且不應從密碼欄位移走焦點。
  • 「Email 或密碼錯誤」的訊息不應具體說明哪一項錯誤。
  • 伺服器端錯誤需根據錯誤碼對應到對應的欄位;通用的錯誤應顯示在表單上方。

10. 資料與日誌

  • 與使用者相關的回應不應進入共用快取("use cache"、CDN)。伺服器上的 QueryClient 應在每個請求中重新建立。
  • 日誌中不得包含 Token、密碼、Email,最多只能記錄使用者 ID 與錯誤的 kind。
  • Session Token 的 payload 應最小化:僅包含 ID 與角色,不包含個人資料。

11. 測試

  • 單元測試: getSafeRedirect(完整測試案例來自 references);「未授權」判定(401、GraphQL 錯誤碼、空主體、非 JSON 回應)。
  • 單次飛行測試: N 個同時收到 401 的請求 → 只發送一個 Refresh,N 次重試;Refresh 被拒絕 → 沒有重試,只呼叫一次 onSessionExpired;Refresh 因網路錯誤失敗 → 回傳 network 錯誤,不登出;Refresh 成功後重試又收到 401 → 不再次 Refresh;在登出期間 → 不執行 Refresh。
  • Route Handler 測試: Cookie 旗標設定正確;Refresh 被拒絕 → Cookie 被刪除;後端無法連線 → Cookie 保留,回應 503;請求主體格式錯誤 → 回應 400;來源 Origin 不符 → 回應 403。
  • 端到端測試: 登入 → 訪問受保護頁面 → 登出 → 無法訪問受保護頁面;Access Token 過期(測試環境中設定短 TTL)→ 隱式 Refresh;密碼重設連結 → Token 從網址列消失。

12. 完成定義

  • 沒有任何 Token 可從 JavaScript 讀取;Session 不使用 localStorage。
  • Cookie 已設定:HttpOnly、正式環境的 Secure、SameSite、Path、生命週期基於 Token。
  • Refresh 流程能區分後端拒絕與後端無法連線兩種情況。
  • 單次飛行機制,包含結果處理、單次重試、登出時封鎖 — 已由測試覆蓋。
  • proxy.ts 僅做樂觀檢查;DAL 在 handlers、actions 與 server components 中呼叫。
  • getSafeRedirect 包含完整的測試案例。
  • 登出時清除客戶端快取與 Store。
  • 連結中的 Token 已從網址列移除。
  • 日誌中不包含 Token 與個人資料。
  • 模型與代理選擇已記錄在 ADR 中。

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

rbikbov/frontend-starter

Creates Architecture Decision Records documenting key technical decisions with context, alternatives considered, tradeoffs, consequences, and decision owners. Use when documenting "architecture decisions", "technical choices", "design decisions", or "ADRs".

rbikbov/frontend-starter

Проверка фронтенд-кода и документации на «нейрослоп» перед коммитом, PR или публикацией репозитория: остатки генераторов, мёртвые и недоделанные опции, утверждения в README и комментариях, не подтверждённые кодом, маркетинговый тон и эмодзи, лишние абстракции, хрупкая обработка ошибок, персональные данные в логах, несуществующие классы стилей. Использовать, когда просят проверить код или репозиторий «на слоп», подготовить проект к публикации или показу, сделать ревью кода, написанного с помощью ИИ, или перед тем как закоммитить собственную большую правку.

rbikbov/frontend-starter

Процесс разработки фичи с агентом: PRD с Non-Goals → компактная спецификация → задачи по одной на коммит → read-only исследование → tracer bullet → сборка с тестом сначала → независимое ревью субагентом → проверка в браузере. Использовать, когда просят сделать фичу, экран, пользовательский сценарий или заметное изменение поведения; когда нужно написать PRD, спецификацию или разбить работу на задачи; когда работа больше одного коммита. Не использовать для опечаток и однострочных правок.

rbikbov/frontend-starter

Старт нового фронтенд-проекта на Next.js (App Router) + TypeScript: фундамент, который закладывается до первой фичи. Использовать, когда нужно: создать проект с нуля; решить, нужны ли Next.js, монорепо, BFF, микрофронтенды; разложить FSD поверх роутинга Next; завести строгий TS, линтер границ, env-конфиг, модель ошибок и API-клиент, тесты, git-хуки, CI, каркас README; навести порядок в молодом проекте после генератора.

相關技能