Skip to main content

验证

了解身份验证对于保护应用的数据至关重要。本页面将指导你了解用于实现身份验证的 React 和 Next.js 功能。

¥Understanding authentication is crucial for protecting your application's data. This page will guide you through what React and Next.js features to use to implement auth.

在开始之前,将流程分解为三个概念会有所帮助:

¥Before starting, it helps to break down the process into three concepts:

  1. 验证:验证用户是否是他们所说的人。它要求用户使用他们拥有的东西来证明他们的身份,例如用户名和密码。

    ¥**Authentication**: Verifies if the user is who they say they are. It requires the user to prove their identity with something they have, such as a username and password.

  2. 会话管理:跨请求跟踪用户的身份验证状态。

    ¥**Session Management**: Tracks the user's auth state across requests.

  3. 授权:决定用户可以访问哪些路由和数据。

    ¥**Authorization**: Decides what routes and data the user can access.

下图显示了使用 React 和 Next.js 功能的身份验证流程:

¥This diagram shows the authentication flow using React and Next.js features:

本页面上的示例演示了用于教育目的的基本用户名和密码身份验证。虽然你可以实现自定义身份验证解决方案,但为了提高安全性和简单性,我们建议使用身份验证库。它们提供了用于身份验证、会话管理和授权的内置解决方案,以及社交登录、多因素身份验证和基于角色的访问控制等附加功能。你可以在 授权库 部分找到列表。

¥The examples on this page walk through basic username and password auth for educational purposes. While you can implement a custom auth solution, for increased security and simplicity, we recommend using an authentication library. These offer built-in solutions for authentication, session management, and authorization, as well as additional features such as social logins, multi-factor authentication, and role-based access control. You can find a list in the Auth Libraries section.

验证

¥Authentication

注册和登录功能

¥Sign-up and login functionality

你可以将 <form> 元素与 React 的 服务器操作useActionState() 一起使用来捕获用户凭据、验证表单字段并调用身份验证提供程序的 API 或数据库。

¥You can use the <form> element with React's Server Actions and useActionState() to capture user credentials, validate form fields, and call your Authentication Provider's API or database.

由于服务器操作始终在服务器上执行,因此它们为处理身份验证逻辑提供了安全的环境。

¥Since Server Actions always execute on the server, they provide a secure environment for handling authentication logic.

以下是实现注册/登录功能的步骤:

¥Here are the steps to implement signup/login functionality:

1. 捕获用户凭据

¥ Capture user credentials

要捕获用户凭据,请创建一个在提交时调用服务器操作的表单。例如,接受用户名、电子邮件和密码的注册表单:

¥To capture user credentials, create a form that invokes a Server Action on submission. For example, a signup form that accepts the user's name, email, and password:

import { signup } from '@/app/actions/auth'

export function SignupForm() {
return (
<form action={signup}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" placeholder="Email" />
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
<button type="submit">Sign Up</button>
</form>
)
}
import { signup } from '@/app/actions/auth'

export function SignupForm() {
return (
<form action={signup}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" placeholder="Email" />
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
<button type="submit">Sign Up</button>
</form>
)
}
export async function signup(formData: FormData) {}
export async function signup(formData) {}

2. 验证服务器上的表单字段

¥ Validate form fields on the server

使用服务器操作来验证服务器上的表单字段。如果你的身份验证提供程序不提供表单验证,你可以使用架构验证库,例如 佐德是的

¥Use the Server Action to validate the form fields on the server. If your authentication provider doesn't provide form validation, you can use a schema validation library like Zod or Yup.

以 Zod 为例,你可以定义带有适当错误消息的表单架构:

¥Using Zod as an example, you can define a form schema with appropriate error messages:

import { z } from 'zod'

export const SignupFormSchema = z.object({
name: z
.string()
.min(2, { message: 'Name must be at least 2 characters long.' })
.trim(),
email: z.string().email({ message: 'Please enter a valid email.' }).trim(),
password: z
.string()
.min(8, { message: 'Be at least 8 characters long' })
.regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })
.regex(/[0-9]/, { message: 'Contain at least one number.' })
.regex(/[^a-zA-Z0-9]/, {
message: 'Contain at least one special character.',
})
.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: 'Name must be at least 2 characters long.' })
.trim(),
email: z.string().email({ message: 'Please enter a valid email.' }).trim(),
password: z
.string()
.min(8, { message: 'Be at least 8 characters long' })
.regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })
.regex(/[0-9]/, { message: 'Contain at least one number.' })
.regex(/[^a-zA-Z0-9]/, {
message: 'Contain at least one special character.',
})
.trim(),
})

为了防止对身份验证提供程序的 API 或数据库进行不必要的调用,如果任何表单字段与定义的架构不匹配,你可以在服务器操作中尽早进行 return

¥To prevent unnecessary calls to your authentication provider's API or database, you can return early in the Server Action if any form fields do not match the defined schema.

import { SignupFormSchema, FormState } from '@/app/lib/definitions'

export async function signup(state: FormState, formData: FormData) {
// Validate form fields
const validatedFields = SignupFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
})

// If any form fields are invalid, return early
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}

// Call the provider or db to create a user...
}
import { SignupFormSchema } from '@/app/lib/definitions'

export async function signup(state, formData) {
// Validate form fields
const validatedFields = SignupFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
})

// If any form fields are invalid, return early
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}

// Call the provider or db to create a user...
}

回到你的 <SignupForm />,你可以使用 React 的 useActionState() 钩子在表单提交时显示验证错误和待处理状态:

¥Back in your <SignupForm />, you can use React's useActionState() hook to display validation errors and a pending state while the form is submitting:

'use client'

import { useActionState } from 'react'
import { signup } from '@/app/actions/auth'

export function SignupForm() {
const [state, action, pending] = useActionState(signup, undefined)

return (
<form action={action}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
{state?.errors?.name && <p>{state.errors.name}</p>}

<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" placeholder="Email" />
</div>
{state?.errors?.email && <p>{state.errors.email}</p>}

<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
{state?.errors?.password && (
<div>
<p>Password must:</p>
<ul>
{state.errors.password.map((error) => (
<li key={error}>- {error}</li>
))}
</ul>
</div>
)}
<button aria-disabled={pending} type="submit">
{pending ? 'Submitting...' : 'Sign up'}
</button>
</form>
)
}
'use client'

import { useActionState } from 'react'
import { signup } from '@/app/actions/auth'

export function SignupForm() {
const [state, action, pending] = useActionState(signup, undefined)

return (
<form action={action}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="John Doe" />
</div>
{state.errors.name && <p>{state.errors.name}</p>}

<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" placeholder="john@example.com" />
</div>
{state.errors.email && <p>{state.errors.email}</p>}

<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
{state.errors.password && (
<div>
<p>Password must:</p>
<ul>
{state.errors.password.map((error) => (
<li key={error}>- {error}</li>
))}
</ul>
</div>
)}
<button aria-disabled={pending} type="submit">
{pending ? 'Submitting...' : 'Sign up'}
</button>
</form>
)
}

很高兴知道:或者,你可以使用 useFormStatus 钩子来显示待处理状态。

¥Good to know: Alternatively, you can use the useFormStatus hook to display the pending state.

3. 创建用户或检查用户凭据

¥ Create a user or check user credentials

验证表单字段后,你可以创建新的用户账户或通过调用身份验证提供商的 API 或数据库来检查用户是否存在。

¥After validating form fields, you can create a new user account or check if the user exists by calling your authentication provider's API or database.

继续前面的示例:

¥Continuing from the previous example:

export async function signup(state: FormState, formData: FormData) {
// 1. Validate form fields
// ...

// 2. Prepare data for insertion into database
const { name, email, password } = validatedFields.data
// e.g. Hash the user's password before storing it
const hashedPassword = await bcrypt.hash(password, 10)

// 3. Insert the user into the database or call an Auth Library's API
const data = await db
.insert(users)
.values({
name,
email,
password: hashedPassword,
})
.returning({ id: users.id })

const user = data[0]

if (!user) {
return {
message: 'An error occurred while creating your account.',
}
}

// TODO:
// 4. Create user session
// 5. Redirect user
}
export async function signup(state, formData) {
// 1. Validate form fields
// ...

// 2. Prepare data for insertion into database
const { name, email, password } = validatedFields.data
// e.g. Hash the user's password before storing it
const hashedPassword = await bcrypt.hash(password, 10)

// 3. Insert the user into the database or call an 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: 'An error occurred while creating your account.',
}
}

// TODO:
// 4. Create user session
// 5. Redirect user
}

成功创建用户账户或验证用户凭据后,你可以创建会话来管理用户的身份验证状态。根据你的会话管理策略,会话可以存储在 cookie 或数据库中,或两者都存储。继续阅读 会话管理 部分以了解更多信息。

¥After successfully creating the user account or verifying the user credentials, you can create a session to manage the user's auth state. Depending on your session management strategy, the session can be stored in a cookie or database, or both. Continue to the Session Management section to learn more.

提示:

¥Tips:

  • 上面的示例很冗长,因为它出于教育目的分解了身份验证步骤。这凸显了实现你自己的安全解决方案很快就会变得复杂。考虑使用 授权库 来简化流程。

    ¥The example above is verbose since it breaks down the authentication steps for the purpose of education. This highlights that implementing your own secure solution can quickly become complex. Consider using an Auth Library to simplify the process.

  • 为了改善用户体验,你可能需要在注册流程的早期检查重复的电子邮件或用户名。例如,当用户输入用户名或输入字段失去焦点时。这可以帮助防止不必要的表单提交并向用户提供即时反馈。你可以使用 use-debounce 等库来消除请求,以管理这些检查的频率。

    ¥To improve the user experience, you may want to check for duplicate emails or usernames earlier in the registration flow. For example, as the user types in a username or the input field loses focus. This can help prevent unnecessary form submissions and provide immediate feedback to the user. You can debounce requests with libraries such as use-debounce to manage the frequency of these checks.

会话管理

¥Session Management

会话管理可确保跨请求保留用户的身份验证状态。它涉及创建、存储、刷新和删除会话或令牌。

¥Session management ensures that the user's authenticated state is preserved across requests. It involves creating, storing, refreshing, and deleting sessions or tokens.

有两种类型的会话:

¥There are two types of sessions:

  1. 无国籍:会话数据(或令牌)存储在浏览器的 cookie 中。cookie 随每个请求一起发送,从而允许在服务器上验证会话。此方法更简单,但如果实现不正确,安全性可能会降低。

    ¥Stateless: Session data (or a token) is stored in the browser's cookies. The cookie is sent with each request, allowing the session to be verified on the server. This method is simpler, but can be less secure if not implemented correctly.

  2. 数据库:会话数据存储在数据库中,用户的浏览器仅接收加密的会话 ID。此方法更安全,但可能很复杂并且使用更多服务器资源。

    ¥Database: Session data is stored in a database, with the user's browser only receiving the encrypted session ID. This method is more secure, but can be complex and use more server resources.

很高兴知道:虽然你可以使用任一方法或同时使用这两种方法,但我们建议使用会话管理库,例如 iron-session何塞

¥Good to know: While you can use either method, or both, we recommend using session management library such as iron-session or Jose.

无状态会话

¥Stateless Sessions

要创建和管理无状态会话,你需要遵循以下几个步骤:

¥To create and manage stateless sessions, there are a few steps you need to follow:

  1. 生成一个密钥,该密钥将用于对你的会话进行签名,并将其存储为 环境变量

    ¥Generate a secret key, which will be used to sign your session, and store it as an environment variable.

  2. 使用会话管理库编写逻辑来加密/解密会话数据。

    ¥Write logic to encrypt/decrypt session data using a session management library.

  3. 使用 Next.js cookies() API 管理 cookie。

    ¥Manage cookies using the Next.js cookies() API.

除了上述内容之外,还可以考虑向 更新(或刷新) 用户返回应用时的会话以及 delete 用户注销时的会话添加功能。

¥In addition to the above, consider adding functionality to update (or refresh) the session when the user returns to the application, and delete the session when the user logs out.

很高兴知道:检查你的 授权库 是否包含会话管理。

¥Good to know: Check if your auth library includes session management.

1. 生成密钥

¥ Generating a secret key

你可以通过多种方式生成密钥来对会话进行签名。例如,你可以选择在终端中使用 openssl 命令:

¥There are a few ways you can generate secret key to sign your session. For example, you may choose to use the openssl command in your terminal:

openssl rand -base64 32

此命令生成一个 32 个字符的随机字符串,你可以将其用作密钥并存储在 环境变量文件 中:

¥This command generates a 32-character random string that you can use as your secret key and store in your environment variables file:

SESSION_SECRET=your_secret_key

然后,你可以在会话管理逻辑中引用此键:

¥You can then reference this key in your session management logic:

const secretKey = process.env.SESSION_SECRET

2. 加密和解密会话

¥ Encrypting and decrypting sessions

接下来,你可以使用你首选的 会话管理库 来加密和解密会话。继续前面的示例,我们将使用 何塞(兼容 Edge 运行时)和 React 的 server-only 包来确保你的会话管理逻辑仅在服务器上执行。

¥Next, you can use your preferred session management library to encrypt and decrypt sessions. Continuing from the previous example, we'll use Jose (compatible with the Edge Runtime) and React's server-only package to ensure that your session management logic is only executed on the server.

import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
import { SessionPayload } from '@/app/lib/definitions'

const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)

export async function encrypt(payload: SessionPayload) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(encodedKey)
}

export async function decrypt(session: string | undefined = '') {
try {
const { payload } = await jwtVerify(session, encodedKey, {
algorithms: ['HS256'],
})
return payload
} catch (error) {
console.log('Failed to verify session')
}
}
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'

const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)

export async function encrypt(payload) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(encodedKey)
}

export async function decrypt(session) {
try {
const { payload } = await jwtVerify(session, encodedKey, {
algorithms: ['HS256'],
})
return payload
} catch (error) {
console.log('Failed to verify session')
}
}

提示:

¥Tips:

  • 有效负载应包含将在后续请求中使用的最少、唯一的用户数据,例如用户的 ID、角色等。它不应包含个人身份信息,例如调用号码、电子邮件地址、信用卡信息等,或者 密码等敏感数据。

    ¥The payload should contain the minimum, unique user data that'll be used in subsequent requests, such as the user's ID, role, etc. It should not contain personally identifiable information like phone number, email address, credit card information, etc, or sensitive data like passwords.

¥ Setting cookies (recommended options)

要将会话存储在 cookie 中,请使用 Next.js cookies() API。应在服务器上设置 cookie,并包含推荐的选项:

¥To store the session in a cookie, use the Next.js cookies() API. The cookie should be set on the server, and include the recommended options:

  • 仅限 Http:阻止客户端 JavaScript 访问 cookie。

    ¥HttpOnly: Prevents client-side JavaScript from accessing the cookie.

  • 安全的:使用 https 发送 cookie。

    ¥Secure: Use https to send the cookie.

  • 同一站点:指定是否可以通过跨站请求发送 cookie。

    ¥SameSite: Specify whether the cookie can be sent with cross-site requests.

  • 最大年龄或过期:一段时间后删除 cookie。

    ¥Max-Age or Expires: Delete the cookie after a certain period.

  • 路径:定义 cookie 的 URL 路径。

    ¥Path: Define the URL path for the cookie.

有关每个选项的更多信息,请参阅 MDN

¥Please refer to MDN for more information on each of these options.

import 'server-only'
import { cookies } from 'next/headers'

export async function createSession(userId: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const session = await encrypt({ userId, expiresAt })

cookies().set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
import 'server-only'
import { cookies } from 'next/headers'

export async function createSession(userId) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const session = await encrypt({ userId, expiresAt })

cookies().set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}

回到服务器操作,你可以调用 createSession() 函数,并使用 redirect() API 将用户重定向到适当的页面:

¥Back in your Server Action, you can invoke the createSession() function, and use the redirect() API to redirect the user to the appropriate page:

import { createSession } from '@/app/lib/session'

export async function signup(state: FormState, formData: FormData) {
// Previous steps:
// 1. Validate form fields
// 2. Prepare data for insertion into database
// 3. Insert the user into the database or call an Library API

// Current steps:
// 4. Create user session
await createSession(user.id)
// 5. Redirect user
redirect('/profile')
}
import { createSession } from '@/app/lib/session'

export async function signup(state, formData) {
// Previous steps:
// 1. Validate form fields
// 2. Prepare data for insertion into database
// 3. Insert the user into the database or call an Library API

// Current steps:
// 4. Create user session
await createSession(user.id)
// 5. Redirect user
redirect('/profile')
}

提示:

¥Tips:

  • 应在服务器上设置 cookie,以防止客户端篡改。

    ¥Cookies should be set on the server to prevent client-side tampering.

  • 🎥 监视:了解有关无状态会话和 Next.js 身份验证的更多信息 → YouTube(11 分钟)

    ¥🎥 Watch: Learn more about stateless sessions and authentication with Next.js → YouTube (11 minutes).

更新(或刷新)会话

¥Updating (or refreshing) sessions

你还可以延长会话的过期时间。这对于用户再次访问应用后保持登录状态非常有用。例如:

¥You can also extend the session's expiration time. This is useful for keeping the user logged in after they access the application again. For example:

import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'

export async function updateSession() {
const session = cookies().get('session')?.value
const payload = await decrypt(session)

if (!session || !payload) {
return null
}

const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
cookies().set('session', session, {
httpOnly: true,
secure: true,
expires: expires,
sameSite: 'lax',
path: '/',
})
}
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'

export async function updateSession() {
const session = cookies().get('session').value
const payload = await decrypt(session)

if (!session || !payload) {
return null
}

const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
cookies().set('session', session, {
httpOnly: true,
secure: true,
expires: expires,
sameSite: 'lax',
path: '/',
})
}

提示:检查你的身份验证库是否支持刷新令牌,该令牌可用于扩展用户的会话。

¥Tip: Check if your auth library supports refresh tokens, which can be used to extend the user's session.

删除会话

¥Deleting the session

要删除会话,可以删除 cookie:

¥To delete the session, you can delete the cookie:

import 'server-only'
import { cookies } from 'next/headers'

export function deleteSession() {
cookies().delete('session')
}
import 'server-only'
import { cookies } from 'next/headers'

export function deleteSession() {
cookies().delete('session')
}

然后你可以在应用中重用 deleteSession() 函数,例如在注销时:

¥Then you can reuse the deleteSession() function in your application, for example, on logout:

import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'

export async function logout() {
deleteSession()
redirect('/login')
}
import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'

export async function logout() {
deleteSession()
redirect('/login')
}

数据库会话

¥Database Sessions

要创建和管理数据库会话,你需要执行以下步骤:

¥To create and manage database sessions, you'll need to follow these steps:

  1. 在数据库中创建一个表来存储会话和数据(或检查你的身份验证库是否处理此问题)。

    ¥Create a table in your database to store session and data (or check if your Auth Library handles this).

  2. 实现插入、更新和删除会话的功能

    ¥Implement functionality to insert, update, and delete sessions

  3. 在将会话 ID 存储到用户浏览器之前对其进行加密,并确保数据库和 cookie 保持同步(这是可选的,但建议用于 中间件 中的乐观身份验证检查)。

    ¥Encrypt the session ID before storing it in the user's browser, and ensure the database and cookie stay in sync (this is optional, but recommended for optimistic auth checks in Middleware).

例如:

¥For example:

import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'

export async function createSession(id: number) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)

// 1. Create a session in the database
const data = await db
.insert(sessions)
.values({
userId: id,
expiresAt,
})
// Return the session ID
.returning({ id: sessions.id })

const sessionId = data[0].id

// 2. Encrypt the session ID
const session = await encrypt({ sessionId, expiresAt })

// 3. Store the session in cookies for optimistic auth checks
cookies().set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'

export async function createSession(id) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)

// 1. Create a session in the database
const data = await db
.insert(sessions)
.values({
userId: id,
expiresAt,
})
// Return the session ID
.returning({ id: sessions.id })

const sessionId = data[0].id

// 2. Encrypt the session ID
const session = await encrypt({ sessionId, expiresAt })

// 3. Store the session in cookies for optimistic auth checks
cookies().set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}

提示:

¥Tips:

  • 为了更快地检索数据,请考虑使用像 维塞尔·Redis 这样的数据库。但是,你也可以将会话数据保留在主数据库中,并合并数据请求以减少查询次数。

    ¥For faster data retrieval, consider using a database like Vercel Redis. However, you can also keep the session data in your primary database, and combine data requests to reduce the number of queries.

  • 你可以选择将数据库会话用于更高级的用例,例如跟踪用户上次登录的时间或活动设备的数量,或者使用户能够注销所有设备。

    ¥You may opt to use database sessions for more advanced use cases, such as keeping track of the last time a user logged in, or number of active devices, or give users the ability to log out of all devices.

实现会话管理后,你需要添加授权逻辑来控制用户可以在应用中访问和执行哪些操作。继续阅读 授权 部分以了解更多信息。

¥After implementing session management, you'll need to add authorization logic to control what users can access and do within your application. Continue to the Authorization section to learn more.

授权

¥Authorization

用户通过身份验证并创建会话后,你可以实现授权来控制用户可以在应用中访问和执行的操作。

¥Once a user is authenticated and a session is created, you can implement authorization to control what the user can access and do within your application.

授权检查主要有两种类型:

¥There are two main types of authorization checks:

  1. 乐观的:检查用户是否有权使用存储在 cookie 中的会话数据访问路由或执行操作。这些检查对于快速操作非常有用,例如显示/隐藏 UI 元素或根据权限或角色重定向用户。

    ¥Optimistic: Checks if the user is authorized to access a route or perform an action using the session data stored in the cookie. These checks are useful for quick operations, such as showing/hiding UI elements or redirecting users based on permissions or roles.

  2. 安全的:检查用户是否有权访问路由或使用数据库中存储的会话数据执行操作。这些检查更加安全,用于需要访问敏感数据或操作的操作。

    ¥Secure: Checks if the user is authorized to access a route or perform an action using the session data stored in the database. These checks are more secure and are used for operations that require access to sensitive data or actions.

对于这两种情况,我们建议:

¥For both cases, we recommend:

使用中间件进行乐观检查(可选)

¥Optimistic checks with Middleware (Optional)

在某些情况下,你可能希望使用 中间件 并根据权限重定向用户:

¥There are some cases where you may want to use Middleware and redirect users based on permissions:

  • 执行乐观检查。由于中间件在每条路由上运行,因此这是集中重定向逻辑和预过滤未经授权的用户的好方法。

    ¥To perform optimistic checks. Since Middleware runs on every route, it's a good way to centralize redirect logic and pre-filter unauthorized users.

  • 保护在用户之间共享数据的静态路由(例如付费专区后面的内容)。

    ¥To protect static routes that share data between users (e.g. content behind a paywall).

然而,由于中间件在每个路由上运行,包括 prefetched 路由,因此仅从 cookie 读取会话(乐观检查)并避免数据库检查以防止性能问题非常重要。

¥However, since Middleware runs on every route, including prefetched routes, it's important to only read the session from the cookie (optimistic checks), and avoid database checks to prevent performance issues.

例如:

¥For example:

import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'

// 1. Specify protected and public routes
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']

export default async function middleware(req: NextRequest) {
// 2. Check if the current route is protected or public
const path = req.nextUrl.pathname
const isProtectedRoute = protectedRoutes.includes(path)
const isPublicRoute = publicRoutes.includes(path)

// 3. Decrypt the session from the cookie
const cookie = cookies().get('session')?.value
const session = await decrypt(cookie)

// 5. Redirect to /login if the user is not authenticated
if (isProtectedRoute && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}

// 6. Redirect to /dashboard if the user is authenticated
if (
isPublicRoute &&
session?.userId &&
!req.nextUrl.pathname.startsWith('/dashboard')
) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
}

return NextResponse.next()
}

// Routes Middleware should not run on
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
import { NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'

// 1. Specify protected and public routes
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']

export default async function middleware(req) {
// 2. Check if the current route is protected or public
const path = req.nextUrl.pathname
const isProtectedRoute = protectedRoutes.includes(path)
const isPublicRoute = publicRoutes.includes(path)

// 3. Decrypt the session from the cookie
const cookie = cookies().get('session')?.value
const session = await decrypt(cookie)

// 5. Redirect to /login if the user is not authenticated
if (isProtectedRoute && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}

// 6. Redirect to /dashboard if the user is authenticated
if (
isPublicRoute &&
session?.userId &&
!req.nextUrl.pathname.startsWith('/dashboard')
) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
}

return NextResponse.next()
}

// Routes Middleware should not run on
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

虽然中间件对于初始检查很有用,但它不应该是保护数据的唯一防线。大多数安全检查应尽可能靠近你的数据源执行,请参阅 数据访问层 了解更多信息。

¥While Middleware can be useful for initial checks, it should not be your only line of defense in protecting your data. The majority of security checks should be performed as close as possible to your data source, see Data Access Layer for more information.

提示:

¥Tips:

  • 在中间件中,你还可以使用 req.cookies.get('session).value 读取 cookie。

    ¥In Middleware, you can also read cookies using req.cookies.get('session).value.

  • 中间件使用 Edge 运行时,检查你的 Auth 库和会话管理库是否兼容。

    ¥Middleware uses the Edge Runtime, check if your Auth library and session management library are compatible.

  • 你可以使用中间件中的 matcher 属性来指定中间件应在哪些路由上运行。不过,对于身份验证,建议中间件在所有路由上运行。

    ¥You can use the matcher property in the Middleware to specify which routes Middleware should run on. Although, for auth, it's recommended Middleware runs on all routes.

创建数据访问层 (DAL)

¥Creating a Data Access Layer (DAL)

我们建议创建 DAL 来集中你的数据请求和授权逻辑。

¥We recommend creating a DAL to centralize your data requests and authorization logic.

DAL 应包含一个函数,用于在用户与应用交互时验证用户的会话。至少,该函数应该检查会话是否有效,然后重定向或返回发出进一步请求所需的用户信息。

¥The DAL should include a function that verifies the user's session as they interact with your application. At the very least, the function should check if the session is valid, then redirect or return the user information needed to make further requests.

例如,为包含 verifySession() 函数的 DAL 创建一个单独的文件。然后使用 React 的 cache API 在 React 渲染过程中记住函数的返回值:

¥For example, create a separate file for your DAL that includes a verifySession() function. Then use React's cache API to memoize the return value of the function during a React render pass:

import 'server-only'

import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'

export const verifySession = cache(async () => {
const cookie = cookies().get('session')?.value
const session = await decrypt(cookie)

if (!session?.userId) {
redirect('/login')
}

return { isAuth: true, userId: session.userId }
})
import 'server-only'

import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'

export const verifySession = cache(async () => {
const cookie = cookies().get('session').value
const session = await decrypt(cookie)

if (!session.userId) {
redirect('/login')
}

return { isAuth: true, userId: session.userId }
})

然后,你可以在数据请求、服务器操作、路由处理程序中调用 verifySession() 函数:

¥You can then invoke the verifySession() function in your data requests, Server Actions, Route Handlers:

export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null

try {
const data = await db.query.users.findMany({
where: eq(users.id, session.userId),
// Explicitly return the columns you need rather than the whole user object
columns: {
id: true,
name: true,
email: true,
},
})

const user = data[0]

return user
} catch (error) {
console.log('Failed to fetch user')
return null
}
})
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null

try {
const data = await db.query.users.findMany({
where: eq(users.id, session.userId),
// Explicitly return the columns you need rather than the whole user object
columns: {
id: true,
name: true,
email: true,
},
})

const user = data[0]

return user
} catch (error) {
console.log('Failed to fetch user')
return null
}
})

提示:

¥Tip:

  • DAL 可用于保护在请求时获取的数据。但是,对于在用户之间共享数据的静态路由,数据将在构建时而不是请求时获取。使用 中间件 来保护静态路由。

    ¥A DAL can be used to protect data fetched at request time. However, for static routes that share data between users, data will be fetched at build time and not at request time. Use Middleware to protect static routes.

  • 为了安全检查,你可以通过将会话 ID 与数据库进行比较来检查会话是否有效。使用 React 的 cache 函数可以避免在渲染过程中对数据库产生不必要的重复请求。

    ¥For secure checks, you can check if the session is valid by comparing the session ID with your database. Use React's cache function to avoid unnecessary duplicate requests to the database during a render pass.

  • 你可能希望将相关数据请求合并到在任何方法之前运行 verifySession() 的 JavaScript 类中。

    ¥You may wish to consolidate related data requests in a JavaScript class that runs verifySession() before any methods.

使用数据传输对象 (DTO)

¥Using Data Transfer Objects (DTO)

检索数据时,建议你仅返回应用中将使用的必要数据,而不是整个对象。例如,如果你要获取用户数据,你可能只返回用户的 ID 和名称,而不是返回可能包含密码、调用号码等的整个用户对象。

¥When retrieving data, it's recommended you return only the necessary data that will be used in your application, and not entire objects. For example, if you're fetching user data, you might only return the user's ID and name, rather than the entire user object which could contain passwords, phone numbers, etc.

但是,如果你无法控制返回的数据结构,或者你所在的团队希望避免将整个对象传递给客户端,则可以使用一些策略,例如指定哪些字段可以安全地公开给客户端。

¥However, if you have no control over the returned data structure, or are working in a team where you want to avoid whole objects being passed to the client, you can use strategies such as specifying what fields are safe to be exposed to the client.

import 'server-only'
import { getUser } from '@/app/lib/dal'

function canSeeUsername(viewer: User) {
return true
}

function canSeePhoneNumber(viewer: User, team: string) {
return viewer.isAdmin || team === viewer.team
}

export async function getProfileDTO(slug: string) {
const data = await db.query.users.findMany({
where: eq(users.slug, slug),
// Return specific columns here
})
const user = data[0]

const currentUser = await getUser(user.id)

// Or return only what's specific to the query here
return {
username: canSeeUsername(currentUser) ? user.username : null,
phonenumber: canSeePhoneNumber(currentUser, user.team)
? user.phonenumber
: null,
}
}
import 'server-only'
import { getUser } from '@/app/lib/dal'

function canSeeUsername(viewer) {
return true
}

function canSeePhoneNumber(viewer, team) {
return viewer.isAdmin || team === viewer.team
}

export async function getProfileDTO(slug) {
const data = await db.query.users.findMany({
where: eq(users.slug, slug),
// Return specific columns here
})
const user = data[0]

const currentUser = await getUser(user.id)

// Or return only what's specific to the query here
return {
username: canSeeUsername(currentUser) ? user.username : null,
phonenumber: canSeePhoneNumber(currentUser, user.team)
? user.phonenumber
: null,
}
}

通过将数据请求和授权逻辑集中在 DAL 中并使用 DTO,你可以确保所有数据请求安全且一致,从而随着应用的扩展更容易维护、审核和调试。

¥By centralizing your data requests and authorization logic in a DAL and using DTOs, you can ensure that all data requests are secure and consistent, making it easier to maintain, audit, and debug as your application scales.

很高兴知道:

¥Good to know:

  • 你可以通过多种不同的方式来定义 DTO,从使用 toJSON() 到单个函数(如上面的示例)或 JS 类。由于这些是 JavaScript 模式,而不是 React 或 Next.js 功能,因此我们建议你进行一些研究,找到最适合你的应用的模式。

    ¥There are a couple of different ways you can define a DTO, from using toJSON(), to individual functions like the example above, or JS classes. Since these are JavaScript patterns and not a React or Next.js feature, we recommend doing some research to find the best pattern for your application.

  • 在我们的 Next.js 中的安全性文章 中了解有关安全最佳实践的更多信息。

    ¥Learn more about security best practices in our Security in Next.js article.

服务器组件

¥Server Components

服务器组件 中的身份验证检查对于基于角色的访问非常有用。例如,根据用户的角色有条件地渲染组件:

¥Auth check in Server Components are useful for role-based access. For example, to conditionally render components based on the user's role:

import { verifySession } from '@/app/lib/dal'

export default function Dashboard() {
const session = await verifySession()
const userRole = session?.user?.role // Assuming 'role' is part of the session object

if (userRole === 'admin') {
return <AdminDashboard />
} else if (userRole === 'user') {
return <UserDashboard />
} else {
redirect('/login')
}
}
import { verifySession } from '@/app/lib/dal'

export default function Dashboard() {
const session = await verifySession()
const userRole = session.role // Assuming 'role' is part of the session object

if (userRole === 'admin') {
return <AdminDashboard />
} else if (userRole === 'user') {
return <UserDashboard />
} else {
redirect('/login')
}
}

在示例中,我们使用 DAL 中的 verifySession() 函数来检查 'admin'、'user' 和未经授权的角色。此模式确保每个用户仅与适合其角色的组件进行交互。

¥In the example, we use the verifySession() function from our DAL to check for 'admin', 'user', and unauthorized roles. This pattern ensures that each user interacts only with components appropriate to their role.

布局和身份验证检查

¥Layouts and auth checks

由于 部分渲染,在 布局 中进行检查时要小心,因为这些检查不会在导航上重新渲染,这意味着不会在每次路由更改时检查用户会话。

¥Due to Partial Rendering, be cautious when doing checks in Layouts as these don't re-render on navigation, meaning the user session won't be checked on every route change.

相反,你应该在数据源或将有条件渲染的组件附近进行检查。

¥Instead, you should do the checks close to your data source or the component that'll be conditionally rendered.

例如,考虑一个获取用户数据并在导航中显示用户图片的共享布局。你应该在布局中获取用户数据 (getUser()) 并在 DAL 中进行身份验证检查,而不是在布局中进行身份验证检查。

¥For example, consider a shared layout that fetches the user data and displays the user image in a nav. Instead of doing the auth check in the layout, you should fetch the user data (getUser()) in the layout and do the auth check in your DAL.

这保证了应用中无论何时调用 getUser(),都会执行身份验证检查,并防止开发者忘记检查用户是否有权访问数据。

¥This guarantees that wherever getUser() is called within your application, the auth check is performed, and prevents developers forgetting to check the user is authorized to access the data.

export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const user = await getUser();

return (
// ...
)
}
export default async function Layout({ children }) {
const user = await getUser();

return (
// ...
)
}
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null

// Get user ID from session and fetch data
})
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null

// Get user ID from session and fetch data
})

很高兴知道:

¥Good to know:

  • SPA 中的常见模式是,如果用户未获得授权,则在布局或顶层组件中进行 return null。不建议使用此模式,因为 Next.js 应用有多个入口点,这不会阻止访问嵌套路由段和服务器操作。

    ¥A common pattern in SPAs is to return null in a layout or a top-level component if a user is not authorized. This pattern is not not recommended since Next.js applications have multiple entry points, which will not prevent nested route segments and Server Actions from being accessed.

服务器操作

¥Server Actions

使用与面向公众的 API 端点相同的安全考虑来对待 服务器操作,并验证是否允许用户执行突变。

¥Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation.

在下面的示例中,我们在允许操作继续之前检查用户的角色:

¥In the example below, we check the user's role before allowing the action to proceed:

'use server'
import { verifySession } from '@/app/lib/dal'

export async function serverAction(formData: FormData) {
const session = await verifySession()
const userRole = session?.user?.role

// Return early if user is not authorized to perform the action
if (userRole !== 'admin') {
return null
}

// Proceed with the action for authorized users
}
'use server'
import { verifySession } from '@/app/lib/dal'

export async function serverAction() {
const session = await verifySession()
const userRole = session.user.role

// Return early if user is not authorized to perform the action
if (userRole !== 'admin') {
return null
}

// Proceed with the action for authorized users
}

路由处理程序

¥Route Handlers

路由处理程序 视为与面向公众的 API 端点相同的安全考虑因素,并验证是否允许用户访问路由处理程序。

¥Treat Route Handlers with the same security considerations as public-facing API endpoints, and verify if the user is allowed to access the Route Handler.

例如:

¥For example:

import { verifySession } from '@/app/lib/dal'

export async function GET() {
// User authentication and role verification
const session = await verifySession()

// Check if the user is authenticated
if (!session) {
// User is not authenticated
return new Response(null, { status: 401 })
}

// Check if the user has the 'admin' role
if (session.user.role !== 'admin') {
// User is authenticated but does not have the right permissions
return new Response(null, { status: 403 })
}

// Continue for authorized users
}
import { verifySession } from '@/app/lib/dal'

export async function GET() {
// User authentication and role verification
const session = await verifySession()

// Check if the user is authenticated
if (!session) {
// User is not authenticated
return new Response(null, { status: 401 })
}

// Check if the user has the 'admin' role
if (session.user.role !== 'admin') {
// User is authenticated but does not have the right permissions
return new Response(null, { status: 403 })
}

// Continue for authorized users
}

上面的示例演示了具有两层安全检查的路由处理程序。它首先检查活动会话,然后验证登录用户是否为 'admin'。

¥The example above demonstrates a Route Handler with a two-tier security check. It first checks for an active session, and then verifies if the logged-in user is an 'admin'.

上下文提供者

¥Context Providers

由于 interleaving,使用上下文提供程序进行身份验证工作。但是,服务器组件不支持 React context,因此它们仅适用于客户端组件。

¥Using context providers for auth work due to interleaving. However, React context is not supported in Server Components, making them only applicable to Client Components.

这是可行的,但任何子服务器组件都将首先在服务器上渲染,并且无法访问上下文提供者的会话数据:

¥This works, but any child Server Components will be rendered on the server first, and will not have access to the context provider’s session data:

import { ContextProvider } from 'auth-lib'

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<ContextProvider>{children}</ContextProvider>
</body>
</html>
)
}
"use client";

import { useSession } from "auth-lib";

export default function Profile() {
const { userId } = useSession();
const { data } = useSWR(`/api/user/${userId}`, fetcher)

return (
// ...
);
}
"use client";

import { useSession } from "auth-lib";

export default function Profile() {
const { userId } = useSession();
const { data } = useSWR(`/api/user/${userId}`, fetcher)

return (
// ...
);
}

如果客户端组件中需要会话数据(例如用于客户端数据获取),请使用 React 的 taintUniqueValue API 来防止敏感会话数据暴露给客户端。

¥If session data is needed in Client Components (e.g. for client-side data fetching),use React’s taintUniqueValue API to prevent sensitive session data from being exposed to the client.

资源

¥Resources

现在你已经了解了 Next.js 中的身份验证,以下是与 Next.js 兼容的库和资源,可帮助你实现安全身份验证和会话管理:

¥Now that you've learned about authentication in Next.js, here are Next.js-compatible libraries and resources to help you implement secure authentication and session management:

授权库

¥Auth Libraries

会话管理库

¥Session Management Libraries

进一步阅读

¥Further Reading

要继续了解身份验证和安全性,请查看以下资源:

¥To continue learning about authentication and security, check out the following resources: