身份认证(Authentication)
理解身份认证对于保护应用程序的数据至关重要。本页将指导你使用 React 和 Next.js 的功能来实现身份认证。
在开始之前,我们可以将这个过程分解为三个概念:
- 身份认证:验证用户的身份是否属实。要求用户提供他们所拥有的信息来证明身份,比如用户名和密码。
- 会话管理:在请求之间跟踪用户的认证状态。
- 授权:决定用户可以访问哪些路由和数据。
下面这张图展示了使用 React 和 Next.js 功能的身份认证流程:
本页的示例将演示基本的用户名和密码认证,用于教育目的。虽然你可以实现自定义身份认证解决方案,但为了提高安全性和简便性,我们建议使用身份认证库。这些库提供了内置的身份认证、会话管理和授权解决方案,以及其他功能,如社交登录、多因素认证和基于角色的访问控制。你可以在 身份认证库 部分找到相关列表。
你可以使用 <form>
元素配合 React 的 服务器操作 和 useFormState
来捕获用户凭据、验证表单字段,并调用你的身份认证提供商的 API 或数据库。
由于服务器操作总是在服务器上执行,因此它们为处理身份认证逻辑提供了一个安全的环境。
以下是实现注册/登录功能的步骤:
要捕获用户凭据,创建一个在提交时调用服务器操作的表单。例如,一个接受用户姓名、电子邮件和密码的注册表单:
使用服务器操作来验证服务器上的表单字段。如果你的身份验证提供商不提供表单验证,你可以使用模式验证库,如 Zod 或 Yup。
以 Zod 为例,你可以定义一个带有适当错误消息的表单模式:
为了避免不必要地调用你的身份验证提供商的 API 或数据库,如果任何表单字段与定义的模式不匹配,你可以在服务器操作中提前 return
。
在你的 <SignupForm />
中,你可以使用 React 的 useFormState
hook 在表单提交时显示验证错误:
值得注意的是:
- 这些示例使用 React 的
useFormState
hook,它已经与 Next.js App Router 捆绑在一起。如果你使用的是 React 19,请使用 useActionState
。详见 React 文档。
- 在 React 19 中,
useFormStatus
在返回的对象中包含额外的键,如 data、method 和 action。如果你没有使用 React 19,则只有 pending
键可用。
- 在 React 19 中,
useActionState
在返回的状态中也包含一个 pending
键。
- 在修改数据之前,你应该始终确保用户也有权执行该操作。参见 身份认证和授权。
在验证表单字段后,你可以通过调用你的身份验证提供商的 API 或数据库来创建新的用户账户或检查用户是否存在。
继续上一个示例:
在成功创建用户账户或验证用户凭据后,你可以创建一个会话来管理用户的认证状态。根据你的会话管理策略,会话可以存储在 cookie 或数据库中,或者两者都有。继续阅读 会话管理 部分了解更多信息。
提示:
- 上面的示例很详细,因为它为了教育目的分解了身份认证步骤。这突显了实现自己的安全解决方案可能很快变得复杂。考虑使用 身份认证库 来简化这个过程。
- 为了改善用户体验,你可能希望在注册流程的早期检查重复的电子邮件或用户名。例如,当用户输入用户名或输入字段失去焦点时。这可以帮助防止不必要的表单提交,并为用户提供即时反馈。你可以使用 use-debounce 等库来管理这些检查的频率。
会话管理确保用户的认证状态在请求之间得到保持。它涉及创建、存储、刷新和删除会话或令牌。
有两种类型的会话:
- 无状态会话:会话数据(或令牌)存储在浏览器的 cookies 中。cookie 随每个请求发送,允许在服务器上验证会话。这种方法更简单,但如果实现不正确,可能不太安全。
- 数据库会话:会话数据存储在数据库中,用户的浏览器只接收加密的会话 ID。这种方法更安全,但可能复杂并使用更多服务器资源。
值得注意的是: 虽然你可以使用任一方法,或两者都用,但我们建议使用会话管理库,如 iron-session 或 Jose。
要创建和管理无状态会话,你需要遵循以下几个步骤:
- 生成一个密钥,用于签署你的会话,并将其存储为 环境变量。
- 使用会话管理库编写加密/解密会话数据的逻辑。
- 使用 Next.js
cookies
API 管理 cookies。
除了上述内容外,还要考虑添加功能来 更新(或刷新) 会话,当用户返回应用程序时,以及 删除 会话,当用户退出登录时。
值得注意的是: 检查你的 身份认证库 是否包含会话管理。
有几种方法可以生成用于签署会话的密钥。例如,你可以选择在终端中使用 openssl
命令:
这个命令生成一个 32 个字符的随机字符串,你可以将其用作密钥并存储在你的 环境变量文件 中:
然后你可以在会话管理逻辑中引用这个密钥:
接下来,你可以使用你偏好的 会话管理库 来加密和解密会话。继续之前的示例,我们将使用 Jose(兼容 Edge Runtime)和 React 的 server-only
包来确保你的会话管理逻辑只在服务器上执行。
提示:
- payload 应该包含将在后续请求中使用的最少、唯一的用户数据,例如用户的 ID、角色等。它不应该包含个人身份信息,如电话号码、电子邮件地址、信用卡信息等,或敏感数据如密码。
要将会话存储在 cookie 中,使用 Next.js cookies
API。cookie 应该在服务器上设置,并包含推荐的选项:
- HttpOnly: 防止客户端 JavaScript 访问 cookie。
- Secure: 使用 https 发送 cookie。
- SameSite: 指定 cookie 是否可以与跨站请求一起发送。
- Max-Age 或 Expires: 在一定时间后删除 cookie。
- Path: 定义 cookie 的 URL 路径。
请参考 MDN 了解有关这些选项的更多信息。
在你的服务器操作中,你可以调用 createSession()
函数,并使用 redirect()
API 将用户重定向到适当的页面:
提示:
- Cookies 应该在服务器上设置以防止客户端篡改。
- 🎥 观看:了解更多关于无状态会话和 Next.js 身份认证的内容 → YouTube (11 分钟)。
你还可以延长会话的过期时间。这对于在用户再次访问应用程序后保持登录状态很有用。例如:
提示: 检查你的身份认证库是否支持刷新令牌,这可用于延长用户的会话。
要删除会话,你可以删除 cookie:
然后你可以在应用程序中重复使用 deleteSession()
函数,例如在退出登录时:
要创建和管理数据库会话,你需要遵循以下步骤:
- 在数据库中创建一个表来存储会话和数据(或检查你的身份认证库是否处理这个)。
- 实现插入、更新和删除会话的功能。
- 在存储到用户浏览器之前加密会话 ID,并确保数据库和 cookie 保持同步(这是可选的,但建议用于在 Middleware 中进行乐观认证检查)。
例如:
提示:
- 为了更快的数据检索,考虑使用像 Vercel Redis 这样的数据库。不过,你也可以将会话数据保存在你的主数据库中,并结合数据请求以减少查询次数。
- 你可能会选择使用数据库会话来处理更高级的用例,比如跟踪用户最后一次登录的时间,或活动设备的数量,或者让用户能够从所有设备登出。
在实现会话管理后,你需要添加授权逻辑来控制用户可以在你的应用程序中访问和执行什么操作。继续阅读 授权 部分了解更多信息。
一旦用户通过身份认证并创建会话,你可以实现授权来控制用户可以在你的应用程序中访问和执行什么操作。
有两种主要类型的授权检查:
- 乐观型:使用存储在 cookie 中的会话数据检查用户是否有权访问路由或执行操作。这些检查对于快速操作很有用,比如根据权限或角色显示/隐藏 UI 元素或重定向用户。
- 安全型:使用存储在数据库中的会话数据检查用户是否有权访问路由或执行操作。这些检查更安全,用于需要访问敏感数据或操作的操作。
对于这两种情况,我们建议:
在某些情况下,你可能想要使用 Middleware 并根据权限重定向用户:
- 执行乐观检查。由于 Middleware 在每个路由上运行,它是集中重定向逻辑和预过滤未授权用户的好方法。
- 保护共享数据的静态路由(例如付费墙后面的内容)。
然而,由于 Middleware 在每个路由上运行,包括 预获取 的路由,重要的是只从 cookie 读取会话(乐观检查),并避免数据库检查以防止性能问题。
例如:
虽然 Middleware 对于初始检查很有用,但它不应该是保护数据的唯一防线。大部分安全检查应该在尽可能接近数据源的地方执行,参见 数据访问层 了解更多信息。
提示:
- 在 Middleware 中,你也可以使用
req.cookies.get('session').value
来读取 cookies。
- Middleware 使用 Edge Runtime,检查你的身份认证库和会话管理库是否兼容。
- 你可以在 Middleware 中使用
matcher
属性来指定 Middleware 应该在哪些路由上运行。不过,对于身份认证,建议 Middleware 在所有路由上运行。
我们建议创建 DAL 来集中你的数据请求和授权逻辑。
DAL 应该包含一个在用户与你的应用程序交互时验证用户会话的函数。至少,该函数应该检查会话是否有效,然后重定向或返回进行进一步请求所需的用户信息。
例如,为你的 DAL 创建一个单独的文件,其中包含 verifySession()
函数。然后使用 React 的 cache API 在 React 渲染过程中记住函数的返回值:
你可以在你的数据请求、服务器操作、路由处理程序中调用 verifySession()
函数:
提示:
- DAL 可用于保护在请求时获取的数据。但是,对于在用户之间共享数据的静态路由,数据将在构建时而不是请求时获取。使用 Middleware 来保护静态路由。
- 对于安全检查,你可以通过将会话 ID 与你的数据库进行比较来检查会话是否有效。使用 React 的 cache 函数来避免在渲染过程中对数据库进行不必要的重复请求。
- 你可能希望将相关的数据请求整合到一个在任何方法之前运行
verifySession()
的 JavaScript 类中。
在检索数据时,建议你只返回将在应用程序中使用的必要数据,而不是整个对象。例如,如果你正在获取用户数据,你可能只返回用户的 ID 和姓名,而不是可能包含密码、电话号码等的整个用户对象。
然而,如果你无法控制返回的数据结构,或者在团队中工作时希望避免将整个对象传递给客户端,你可以使用策略,如指定哪些字段可以安全地暴露给客户端。
通过在 DAL 中集中你的数据请求和授权逻辑并使用 DTO,你可以确保所有数据请求都是安全和一致的,这使得随着应用程序的扩展更容易维护、审计和调试。
值得注意的是:
- 有几种不同的方式可以定义 DTO,从使用
toJSON()
,到上面示例中的单独函数,或者 JS 类。由于这些是 JavaScript 模式而不是 React 或 Next.js 功能,我们建议你研究找到最适合你的应用程序的模式。
- 在我们的 Next.js 中的安全文章 中了解更多关于安全最佳实践的信息。
在 服务器组件 中进行身份认证检查对于基于角色的访问很有用。例如,根据用户的角色条件渲染组件:
在这个示例中,我们使用 DAL 中的 verifySession()
函数来检查 'admin'、'user' 和未授权的角色。这种模式确保每个用户只能与适合其角色的组件交互。
由于 部分渲染,在 布局 中进行检查时要谨慎,因为这些在导航时不会重新渲染,意味着用户会话不会在每次路由变更时都被检查。
相反,你应该在靠近数据源或将被条件渲染的组件处进行检查。
例如,考虑一个共享布局,它获取用户数据并在导航栏中显示用户图像。与其在布局中进行身份认证检查,你应该在布局中获取用户数据(getUser()
),并在你的 DAL 中进行身份认证检查。
这保证了在应用程序中的任何地方调用 getUser()
时都会执行身份认证检查,并防止开发人员忘记检查用户是否有权访问数据。
值得注意的是:
- SPA 中的一个常见模式是如果用户未经授权,则在布局或顶级组件中
return null
。由于 Next.js 应用程序有多个入口点,这种模式是不推荐的,因为它不会阻止嵌套路由段和服务器操作被访问。
对 服务器操作 的处理应与面向公众的 API 端点具有相同的安全考虑,并验证用户是否被允许执行变更。
在下面的示例中,我们在允许操作继续之前检查用户的角色:
对 路由处理程序 的处理应与面向公众的 API 端点具有相同的安全考虑,并验证用户是否有权访问路由处理程序。
例如:
上面的示例演示了具有两层安全检查的路由处理程序。它首先检查活动会话,然后验证登录用户是否是 'admin'。
由于 交错,使用上下文提供者进行身份认证是可行的。然而,React 的 context
在服务器组件中不受支持,使其仅适用于客户端组件。
这是可行的,但任何子服务器组件将首先在服务器上渲染,并且无法访问上下文提供者的会话数据:
如果客户端组件需要会话数据(例如,用于客户端数据获取),使用 React 的 taintUniqueValue
API 来防止敏感的会话数据暴露给客户端。
现在你已经了解了 Next.js 中的身份认证,以下是兼容 Next.js 的库和资源,可以帮助你实现安全的身份认证和会话管理:
要继续了解身份认证和安全性,请查看以下资源: