如何在 Next.js 中实现身份验证
理解身份验证对于保护你的应用程序数据至关重要。本页将指导你了解使用哪些 React 和 Next.js 功能来实现身份验证。
在开始之前,将过程分解为三个概念会很有帮助:
此图显示了使用 React 和 Next.js 功能的身份验证流程:


本页的示例介绍了用于教育目的的基本用户名和密码身份验证。虽然你可以实现自定义身份验证解决方案,但为了增加安全性和简化性,我们建议使用身份验证库。 这些库提供身份验证、会话管理和授权的内置解决方案,以及社交登录、多因素身份验证和基于角色的访问控制等附加功能。你可以在身份验证库部分找到列表。
身份验证
注册和登录功能
你可以使用 <form> 元素与 React 的服务端操作和 useActionState 来捕获用户凭据、验证表单字段并调用你的身份验证提供商的 API 或数据库。
由于服务端操作始终在服务器上执行,它们为处理身份验证逻辑提供了安全的环境。
以下是实现注册/登录功能的步骤:
1. 捕获用户凭据
要捕获用户凭据,创建一个在提交时调用服务端操作的表单。例如,一个接受用户姓名、邮箱和密码的注册表单:
- TypeScript
- JavaScript
import { signup } from '@/app/actions/auth'
export function SignupForm() {
return (
<form action={signup}>
<div>
<label htmlFor="name">姓名</label>
<input id="name" name="name" placeholder="姓名" />
</div>
<div>
<label htmlFor="email">邮箱</label>
<input id="email" name="email" type="email" placeholder="邮箱" />
</div>
<div>
<label htmlFor="password">密码</label>
<input id="password" name="password" type="password" />
</div>
<button type="submit">注册</button>
</form>
)
}
import { signup } from '@/app/actions/auth'
export function SignupForm() {
return (
<form action={signup}>
<div>
<label htmlFor="name">姓名</label>
<input id="name" name="name" placeholder="姓名" />
</div>
<div>
<label htmlFor="email">邮箱</label>
<input id="email" name="email" type="email" placeholder="邮箱" />
</div>
<div>
<label htmlFor="password">密码</label>
<input id="password" name="password" type="password" />
</div>
<button type="submit">注册</button>
</form>
)
}
- TypeScript
- JavaScript
export async function signup(formData: FormData) {}
export async function signup(formData) {}
2. 在服务器上验证表单字段
使用服务端操作在服务器上验证表单字段。如果你的身份验证提供商不提供表单验证,你可以使用模式验证库,如 Zod 或 Yup。
以 Zod 为例,你可以定义一个带有适当错误消息的表单模式:
- TypeScript
- JavaScript
import { z } from 'zod'
export const SignupFormSchema = z.object({
name: z
.string()
.min(2, { message: '姓名必须至少包含 2 个字符。' })
.trim(),
email: z.string().email({ message: '请输入有效的邮箱地址。' }).trim(),
password: z
.string()
.min(8, { message: '至少包含 8 个字符' })
.regex(/[a-zA-Z]/, { message: '至少包含一个字母。' })
.regex(/[0-9]/, { message: '至少包含一个数字。' })
.regex(/[^a-zA-Z0-9]/, {
message: '至少包含一个特殊字符。',
})
.trim(),
})
export type FormState =
| {
errors?: {
name?: string[]
email?: string[]
password?: string[]
}
message?: string
}
| undefined
import { z } from 'zod'
export const SignupFormSchema = z.object({
name: z
.string()
.min(2, { message: '姓名必须至少包含 2 个字符。' })
.trim(),
email: z.string().email({ message: '请输入有效的邮箱地址。' }).trim(),
password: z
.string()
.min(8, { message: '至少包含 8 个字符' })
.regex(/[a-zA-Z]/, { message: '至少包含一个字母。' })
.regex(/[0-9]/, { message: '至少包含一个数字。' })
.regex(/[^a-zA-Z0-9]/, {
message: '至少包含一个特殊字符。',
})
.trim(),
})
为了防止对身份验证提供商的 API 或数据库进行不必要的调用,如果任何表单字段与定义的模式不匹配,你可以在服务端操作中提前 return。
- TypeScript
- JavaScript
import { SignupFormSchema, FormState } from '@/app/lib/definitions'
export async function signup(state: FormState, formData: FormData) {
// 验证表单字段
const validatedFields = SignupFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
})
// 如果任 何表单字段无效,提前返回
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// 调用提供商或数据库创建用户...
}
import { SignupFormSchema } from '@/app/lib/definitions'
export async function signup(state, formData) {
// 验证表单字段
const validatedFields = SignupFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
})
// 如果任何表单字段无效,提前返回
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// 调用提供商或数据库创建用户...
}
回到你的 <SignupForm /> 中,你可以使用 React 的 useActionState hook 在表单提交时显示验证错误:
- TypeScript
- JavaScript
'use client'
import { signup } from '@/app/actions/auth'
import { useActionState } from 'react'
export default function SignupForm() {
const [state, action, pending] = useActionState(signup, undefined)
return (
<form action={action}>
<div>
<label htmlFor="name">姓名</label>
<input id="name" name="name" placeholder="姓名" />
</div>
{state?.errors?.name && <p>{state.errors.name}</p>}
<div>
<label htmlFor="email">邮箱</label>
<input id="email" name="email" placeholder="邮箱" />
</div>
{state?.errors?.email && <p>{state.errors.email}</p>}
<div>
<label htmlFor="password">密码</label>
<input id="password" name="password" type="password" />
</div>
{state?.errors?.password && (
<div>
<p>密码必须包含:</p>
<ul>
{state.errors.password.map((error) => (
<li key={error}>- {error}</li>
))}
</ul>
</div>
)}
<button disabled={pending} type="submit">
注册
</button>
</form>
)
}
'use client'
import { signup } from '@/app/actions/auth'
import { useActionState } from 'react'
export default function SignupForm() {
const [state, action, pending] = useActionState(signup, undefined)
return (
<form action={action}>
<div>
<label htmlFor="name">姓名</label>
<input id="name" name="name" placeholder="姓名" />
</div>
{state?.errors?.name && <p>{state.errors.name}</p>}
<div>
<label htmlFor="email">邮箱</label>
<input id="email" name="email" placeholder="邮箱" />
</div>
{state?.errors?.email && <p>{state.errors.email}</p>}
<div>
<label htmlFor="password">密码</label>
<input id="password" name="password" type="password" />
</div>
{state?.errors?.password && (
<div>
<p>密码必须包含:</p>
<ul>
{state.errors.password.map((error) => (
<li key={error}>- {error}</li>
))}
</ul>
</div>
)}
<button disabled={pending} type="submit">
注册
</button>
</form>
)
}
注意:
- 在 React 19 中,
useFormStatus返回的对象中包含额外的键,如 data、method 和 action。如果你不使用 React 19,则只有pending键可用。- 在修改数据之前,你应该始终确保用户也授权执行该操作。请参阅 Authentication and Authorization。
3. 创建用户或检查用户凭据
验证表单字段后,你可以创建一个新用户账户或通过调用你的身份验证提供商的 API 或数据库来检查用户是否存在。
继续上一个示例:
- TypeScript
- JavaScript
export async function signup(state: FormState, formData: FormData) {
// 1. 验证表单字段
// ...
// 2. 准备数据以插入数据库
const { name, email, password } = validatedFields.data
// 例如,在存储之前对用户密码进行哈希处理
const hashedPassword = await bcrypt.hash(password, 10)
// 3. 将用户插入数据库或调用 Auth 库的 API
const data = await db
.insert(users)
.values({
name,
email,
password: hashedPassword,
})
.returning({ id: users.id })
const user = data[0]
if (!user) {
return {
message: '创建账户时发生错误。',
}
}
// TODO:
// 4. 创建用户会话
// 5. 重定向用户
}
export async function signup(state, formData) {
// 1. 验证表单字段
// ...
// 2. 准备数据以插入数据库
const { name, email, password } = validatedFields.data
// 例如,在存储之前对用户密码进行哈希处理
const hashedPassword = await bcrypt.hash(password, 10)
// 3. 将用户插入数据库或调用 Library API
const data = await db
.insert(users)
.values({
name,
email,
password: hashedPassword,
})
.returning({ id: users.id })
const user = data[0]
if (!user) {
return {
message: '创建账户时发生错误。',
}
}
// TODO:
// 4. 创建用户会话
// 5. 重定向用户
}
成功创建用户账户或验证用户凭据后,你可以创建一个会话来管理用户的认证状态。根据你的会话管理策略,会话可以存储在 cookie 或数据库中,或两者都存储。继续进入会话管理部分以了解更多信息。
Tips:
- 上述示例由于教育目的而详细说明了认证步骤,这突显了实现自己的安全解决方案可以快速变得复杂。考虑使用身份验证库来简化过程。
- 为了改善用户体验,你可能希望在注册流程的早期检查重复的邮箱或用户名。例如,当用户在用户名中输入或输入框失去焦点时。这可以帮助防止不必要的表单提交并为用户提供即时反馈。你可以使用像 use-debounce 这样的库来管理这些检查的频率。
会话管理
会话管理确保用户认证状态在请求之间得到保留。它涉及创建、存储、刷新和删除会话或令牌。
有两种类型的会话:
- 无状态会话: 会话数据(或令牌)存储在浏览器的 cookie 中。cookie 随每个请求发送,允许服务器验证会话。这种方法更简单,但如果实现不当,则可能不那么安全。
- 数据库会话: 会话数据存储在数据库中,用户的浏览器只接收加密的会话 ID。这种方法更安全,但可能更复杂并使用更多服务器资源。
注意: 虽然你可以使用这两种方法,或两者都使用,但我们建议使用会话管理库,例如 iron-session 或 Jose。
无状态会话
要创建和管理无状态会话,你需要遵循以下步骤:
此外,请考虑添加功能来更新(或刷新)当用户返回应用程序时,以及删除会话。
注意: 检查你的身份验证库是否包含会话管理。