Communitygithub.com

Next.js 前端认证与会话管理指南

一套完整的最佳实践,用于在 Next.js (App Router) 前端中安全地管理认证与会话,涵盖令牌存储、BFF 模式、无竞态刷新、安全重定向与退出等核心能力。

Next.js 前端认证与会话管理指南 是什么?

此技能提供了一套在 Next.js (App Router) 应用中设计和实现前端认证与会话管理的全面指南。它涵盖了从选择合适模型(如 BFF 模式、HttpOnly Cookie)到具体实现的方方面面。核心内容包括:令牌的安全存储(强制使用 HttpOnly Cookie)、通过 Route Handlers 构建 BFF 层、在 `proxy.ts` 和数据访问层进行多级会话校验、实现无竞态的令牌刷新(single-flight 模式),以及确保安全重定向和退出时不泄露数据。该指南还特别处理了 GraphQL 的 200 状态码认证错误识别、密码重置链接中的令牌清理、以及健壮的测试策略。它适用于 Next.js 项目,并与所有主要认证库(Auth.js、Clerk 等)兼容,是开发或审查认证相关代码时的权威参考。

兼容平台~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 中的认证与会话

本文档面向从后端获取会话(令牌或会话 ID)并需要存储、续期和验证会话的前端应用。如果项目使用了某个库或提供商(Auth.js、Better Auth、Clerk 等),会话机制由其接管,但第 2、6–10 节的规则仍然适用。代码示例请参考 references/implementation.md。

规则优先级。 用户指令和项目 agent 文件优先级更高。如果后端有不同要求(例如,自行设置 cookie),则遵循后端,并将差异记录在 README 或 ADR 中。

标注说明:MUST — 必须遵守,不可违反;SHOULD — 默认情况下应遵守,如有偏离需说明理由。


1. 模型选择

模型适用场景
BFF: Next Route Handlers 从后端获取令牌并存入 HttpOnly Cookie后端在响应体中返回令牌。Next 项目的默认选项
后端自行设置 HttpOnly Cookie前端和 API 位于同一站点,后端由你控制
库或提供商需要 OAuth、MFA、用户管理——不要自行实现
Access 令牌在内存中,Refresh 令牌在 HttpOnly Cookie 中没有 BFF 的 SPA
令牌存储在 localStorage/sessionStorage禁止: 任何 XSS 攻击都会泄露会话

第二个选择是 BFF 代理哪些请求:

方案优点缺点
仅代理 login / refresh / logout,其余请求自带 cookie 直接访问 APIBFF 不是瓶颈;代码量更少API 需要从 cookie 中读取令牌并配置 CORS;cookie 必须能送达 API 的域名
所有请求都通过 BFFAPI 无需关心 cookie 和 CORS;单一源每个请求都多一次跳转;BFF 需要扩展

选择结果需记录在 ADR 中。


2. Cookie(MUST)

  • 所有能提供访问权限的令牌(access、refresh、会话 ID)都必须设置 HttpOnly。
  • 生产环境必须设置 Secure。本地开发时,浏览器允许在 http://localhost 上使用 Secure,但在其他 http 主机上不允许。
  • 默认使用 SameSite=Lax。Strict 对 CSRF 防护更强,但用户通过邮件或外部网站链接访问时,首次点击会显示未认证状态——需有意识地选择并记录原因。
  • 设置 Path=/;过期时间取自令牌的 exp 字段或后端响应,不可随意设定。
  • SHOULD:在 cookie 名称中使用 __Host- 前缀(要求 Secure、Path=/、不含 Domain)——防止从子域覆盖 cookie。
  • Cookie 的设置和删除只能由服务端代码(Route Handler、Server Action、proxy.ts)完成。Server Component 不能设置 cookie。
  • 仅能通过 JS 读取的 cookie 不是会话载体。像 is_authenticated 这样的标志(不含 HttpOnly)只能作为 UI 提示,绝不能用于访问决策。

3. BFF:Route Handlers(MUST)

  • 登录接口必须使用 schema(zod)验证请求体;格式不正确则返回 400。
  • 修改数据的处理程序必须检查 Origin 头是否等于应用的 origin:SameSite=Lax 无法防御来自同一站点子域的请求。Server Actions 会自行比较 Origin 与 Host。
  • 客户端的上下文应传递给后端:X-Forwarded-For、User-Agent——否则后端的速率限制只能看到服务器的 IP。
  • 后端错误应转换为带有 kind 和状态码(401、403、422、429 带 Retry-After、503)的响应,不要将后端的原始错误文本暴露出去。
  • 刷新令牌: 如果后端拒绝了 refresh 令牌——删除 cookie 并返回 401。如果后端不可用(网络、5xx)——不要动 cookie,返回 503:临时故障不应导致用户登出。
  • 登出: 在后端撤销令牌(记录日志但不展示错误),无论如何都要删除 cookie。

4. 会话校验位置(MUST)

位置职责不做什么
proxy.ts(Next 16+,旧版为 middleware.ts)乐观性检查:根据是否存在 cookie 跳转到登录页或主页不查询数据库或 API;不是唯一防护
数据访问层(server-only 模块中的 verifySession())在数据附近进行校验:Server Components、Route Handlers、Server Actions—
布局不做任何事:布局在导航时不会重新渲染不检查访问权限
后端真相来源:在每次请求中验证令牌—
  • 每个 Route Handler 和 Server Action 都是公共入口点,必须自行调用 verifySession()。
  • proxy.ts 检查的是 refresh cookie(或会话 ID):access 令牌可能已过期,但会话仍有效。
  • 路径匹配按段进行:/app 和 /app/... 都匹配,但 /apple 不匹配。
  • 如果启用了 cacheComponents(Next 16.4 及之后项目的默认行为),会话读取和用户数据缓存有各自的规则:在编写 DAL 之前,请阅读 node_modules/next/dist/docs/01-app/02-guides/authentication-with-cache-components.md。
  • 权限(角色、归属)必须基于已校验的数据:通过签名(JWKS、jose)或后端响应。未经验证的 JWT payload 不能作为访问依据。
  • Server Components 无法更新 cookie。如果在服务端渲染时 access 令牌已过期:要么由 proxy.ts 更新会话(它可以在响应中设置 cookie——但仅限文档导航,不适用于 prefetch,并需考虑 refresh 令牌轮换),要么页面渲染客户端组件进行加载和刷新。

5. 客户端:无竞态刷新(MUST)

  • “未认证”的判定: HTTP 401,或者在 GraphQL 响应(状态码为 200)中,至少有一个错误的 extensions.code === "UNAUTHENTICATED"(使用 errors.some,而非 errors[0])。如果后端不返回代码,只能与消息文本比较——且必须在单一函数中,附注释和测试。
  • 单次请求(Single-flight): 并发的多个请求收到 401 后,应等待一个共享的 refresh 请求,并使用其结果。Promise 在 finally 中重置。
  • 原始请求只重试一次。如果重试后仍返回 401,则会话已失效,不再进行第二次刷新。
  • refresh 请求本身不应经过拦截器(否则会导致递归)。
  • refresh 的结果必须区分三种情况:成功(true)→ 重试请求;被拒(false)→ 会话过期;网络或 5xx(异常)→ network 错误,不清除用户登录状态。
  • 登出期间应阻止 refresh:否则,在登出瞬间失败的请求可能会立即恢复会话。
  • 会话过期后:清理请求缓存,跳转到登录页,并携带当前页面的 callbackUrl。
  • 被重试的请求体必须是可重复读取的(字符串、FormData),不能是一次性的 ReadableStream。

6. 登录后重定向(MUST)

  • callbackUrl(next、returnTo)必须通过 getSafeRedirect(raw, fallback, appOrigin) 函数校验:拒绝 \ 和控制字符(浏览器会将 /\.evildomain.com 和 /\t/evildomain.com 转换为 //evildomain.com),使用 new URL(raw, appOrigin) 解析,比较 origin 并只返回路径部分。仅检查“以 / 开头且非 //”是不够的。
  • 相应的测试集见 references/implementation.md 第 7 节。
  • 登录后,服务器组件必须能读取到新的 cookie:使用全页面导航(window.location.assign)或 router.replace 加 router.refresh()。

7. 登出(MUST)

  • 服务端删除 cookie(第 3 节)。
  • 客户端清理请求缓存(queryClient.clear())和所有客户端上存储的用户数据:下一个用户在同一标签页中不应看到任何前任的数据,哪怕是一瞬间。
  • 导航到登录页,且不能通过“回退”按钮回到缓存的受保护页面(使用 replace 或全页面导航)。
  • 登出确认是可选的(根据产品需求);但请求期间按钮必须禁用。
  • SHOULD:其他标签页应能得知登出事件(通过 BroadcastChannel 或基于 storage 事件的标志),否则它们会继续展示数据直到遇到第一个 401。

8. 链接中的令牌(密码重置、邮箱确认)

  • 令牌从 URL 中读取一次并存入状态,然后移除令牌后的地址(router.replace(pathname)):令牌不会留在浏览器历史中,也不会通过 Referer 泄露。
  • 这些页面应设置 Referrer-Policy: no-referrer,且不加载任何第三方脚本(分析、插件)。
  • 令牌不应被记录到日志。令牌的一次性和有效期由后端保证;前端只需展示清晰的“链接已过期”界面,并提供“请求新链接”的操作。

9. 登录和密码重置表单

  • 密码重置接口对已存在和不存在的邮箱应返回相同的响应(“如果账户存在,邮件已发送”),不得透露哪些邮箱已注册。如果后端泄露了此信息,应将其记录为限制。
  • 请求发送期间应禁用按钮,防止重复提交。
  • 遇到 429 状态:如果存在 Retry-After 头,则显示等待时间信息。
  • 输入字段:邮箱使用 type="email" 和 autoComplete="email";密码在登录时使用 autoComplete="current-password",在注册和修改密码时使用 "new-password";不应阻止粘贴。
  • “显示密码”按钮应使用 type="button",带有 aria-label 和 aria-pressed,且不应从密码字段夺走焦点。
  • “邮箱或密码错误”这样的提示不应指出具体哪一项错误。
  • 服务端错误应根据其代码映射到对应字段;通用的错误应显示在表单顶部。

10. 数据与日志

  • 依赖于用户身份的响应不应进入公共缓存("use cache"、CDN)。每次新的服务端请求都应该使用新的 QueryClient。
  • 日志中不得包含令牌、密码或邮箱。最多只能记录用户 ID 和错误类型 kind。
  • 会话令牌的 payload 应保持最小:只包含 ID 和角色,不含个人数据。

11. 测试

  • 单元测试: getSafeRedirect(完整的测试集来自 references);“未认证”判定(401、GraphQL 错误码、空响应体、返回 HTML 而非 JSON 的情况)。
  • 单次请求测试: N 个并发请求返回 401 → 只进行一次 refresh,N 次重试;refresh 被拒 → 无重试,onSessionExpired 调用一次;refresh 网络失败 → network 错误,不登出;成功 refresh 后再次遇到 401 → 不进行第二次 refresh;登出期间 → 不发起 refresh。
  • Route Handlers 测试: cookie 的标志位;refresh 被拒 → cookie 被删除;后端不可用 → cookie 保留,返回 503;请求体错误 → 返回 400;Origin 不匹配 → 返回 403。
  • 端到端测试: 登录 → 访问受保护页面 → 登出 → 受保护页面不可访问;access 令牌过期(在测试环境中使用短 TTL)→ 透明地完成 refresh;密码重置链接 → 令牌从地址栏消失。

12. 完成定义

  • 没有令牌可从 JavaScript 读取;localStorage 未用于会话。
  • Cookie 设置了 HttpOnly、Secure(生产环境)、SameSite、Path,过期时间取自令牌。
  • 区分了 refresh 令牌被拒和后端不可达的情况。
  • 单次刷新(single-flight)有明确结果,仅重试一次,登出期间阻止刷新——以上均有测试覆盖。
  • proxy.ts 仅做乐观性检查;实际校验在 Handlers、Actions 和 Server Components 中通过 DAL 完成。
  • getSafeRedirect 带有完整的测试集。
  • 登出会清理客户端的缓存和存储。
  • URL 中的令牌在读取后从地址栏中移除。
  • 日志中不包含令牌或个人数据。
  • 模型选择和代理方案已记录在 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; навести порядок в молодом проекте после генератора.

相关技能