如何在 Next.js 中实现身份验证
理解身份验证机制对于保护应用数据至关重要。本文将指导您使用 React 和 Next.js 特性来实现身份验证功能。
在开始之前,我们可以将整个过程分解为三个核心概念:
- 身份验证 (Authentication):验证用户是否与其声称的身份一致。要求用户通过用户名密码等凭证证明身份。
- 会话管理 (Session Management):跨请求跟踪用户的认证状态。
- 授权 (Authorization):决定用户可以访问哪些路由和数据。
下图展示了使用 React 和 Next.js 特性的身份验证流程:

本文示例将演示基本的用户名密码验证(出于教学目的)。虽然您可以实现自定义验证方案,但为了更高的安全性和简便性,我们推荐使用身份验证库。这些库提供了开箱即用的解决方案,包括身份验证、会话管理、授权,以及社交登录、多因素认证、基于角色的访问控制等附加功能。您可以在 身份验证库 部分找到相关列表。
身份验证
以下是实现注册和/或登录表单的步骤:
- 用户通过表单提交其凭证。
- 表单发送一个由 API 路由处理的请求。
- 验证成功后,流程完成,表示用户已成功认证。
- 如果验证失败,则显示错误消息。
考虑一个用户可以输入其凭证的登录表单:
import { FormEvent } from 'react'
import { useRouter } from 'next/router'
export default function LoginPage() {
const router = useRouter()
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const email = formData.get('email')
const password = formData.get('password')
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
if (response.ok) {
router.push('/profile')
} else {
// 处理错误
}
}
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" placeholder="Email" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit">登录</button>
</form>
)
}
import { FormEvent } from 'react'
import { useRouter } from 'next/router'
export default function LoginPage() {
const router = useRouter()
async function handleSubmit(event) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const email = formData.get('email')
const password = formData.get('password')
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
if (response.ok) {
router.push('/profile')
} else {
// 处理错误
}
}
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" placeholder="Email" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit">登录</button>
</form>
)
}
上面的表单有两个输入字段,用于捕获用户的电子邮件和密码。提交时,它会触发一个函数,向 API 路由 (/api/auth/login
) 发送 POST 请求。
然后,您可以在 API 路由中调用认证提供商的 API 来处理认证:
import type { NextApiRequest, NextApiResponse } from 'next'
import { signIn } from '@/auth'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const { email, password } = req.body
await signIn('credentials', { email, password })
res.status(200).json({ success: true })
} catch (error) {
if (error.type === 'CredentialsSignin') {
res.status(401).json({ error: '无效凭证' })
} else {
res.status(500).json({ error: '发生错误' })
}
}
}
import { signIn } from '@/auth'
export default async function handler(req, res) {
try {
const { email, password } = req.body
await signIn('credentials', { email, password })
res.status(200).json({ success: true })
} catch (error) {
if (error.type === 'CredentialsSignin') {
res.status(401).json({ error: '无效凭证' })
} else {
res.status(500).json({ error: '发生错误' })
}
}
}
会话管理
会话管理确保用户的认证状态在多个请求之间保持。它涉及创建、存储、刷新和删除会话或令牌。
有两种类型的会话:
- 无状态会话:会话数据(或令牌)存储在浏览器的 cookie 中。cookie 随每个请求发送,允许在服务器上验证会话。这种方法更简单,但如果实现不当可能不太安全。
- 数据库会话:会话数据存储在数据库中,用户的浏览器仅接收加密的会话 ID。这种方法更安全,但可能更复杂并占用更多服务器资源。
须知: 虽然您可以使用其中一种方法或两者兼用,但我们建议使用会话管理库,如 iron-session 或 Jose。
无状态会话
设置和删除 Cookie
可以使用 API 路由 在服务端将会话设置为 cookie:
import { serialize } from 'cookie'
import type { NextApiRequest, NextApiResponse } from 'next'
import { encrypt } from '@/app/lib/session'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const sessionData = req.body
const encryptedSessionData = encrypt(sessionData)
const cookie = serialize('session', encryptedSessionData, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 一周
path: '/',
})
res.setHeader('Set-Cookie', cookie)
res.status(200).json({ message: 'Successfully set cookie!' })
}
import { serialize } from 'cookie'
import { encrypt } from '@/app/lib/session'
export default function handler(req, res) {
const sessionData = req.body
const encryptedSessionData = encrypt(sessionData)
const cookie = serialize('session', encryptedSessionData, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 一周
path: '/',
})
res.setHeader('Set-Cookie', cookie)
res.status(200).json({ message: 'Successfully set cookie!' })
}
数据库会话
要创建和管理数据库会话,需要遵循以下步骤:
- 在数据库中创建表来存储会话数据(或检查您的认证库是否已处理此功能)
- 实现插入、更新和删除会话的功能
- 将会话 ID 加密后再存储到用户浏览器中,并确保数据库和 cookie 保持同步(这是可选的,但建议用于 中间件 中的乐观认证检查)
在服务端创建会话:
import db from '../../lib/db'
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const user = req.body
const sessionId = generateSessionId()
await db.insertSession({
sessionId,
userId: user.id,
createdAt: new Date(),
})
res.status(200).json({ sessionId })
} catch (error) {
res.status(500).json({ error: 'Internal Server Error' })
}
}
import db from '../../lib/db'
export default async function handler(req, res) {
try {
const user = req.body
const sessionId = generateSessionId()
await db.insertSession({
sessionId,
userId: user.id,
createdAt: new Date(),
})
res.status(200).json({ sessionId })
} catch (error) {
res.status(500).json({ error: 'Internal Server Error' })
}
}
授权
用户认证并创建会话后,可以实现授权来控制用户在应用中可以访问和执行的内容。
主要有两种授权检查类型:
- 乐观检查:使用存储在 cookie 中的会话数据检查用户是否有权访问路由或执行操作。这些检查适用于快速操作,如显示/隐藏 UI 元素或根据权限或角色重定向用户
- 安全检查:使用存储在数据库中的会话数据检查用户是否有权访问路由或执行操作。这些检查更安全,用于需要访问敏感数据或操作的情况
对于这两种情况,我们建议:
- 创建 数据访问层 (DAL) 来集中授权逻辑
- 使用 数据传输对象 (DTO) 仅返回必要的数据
- 可选使用 中间件 执行乐观检查
使用中间件进行乐观检查(可选)
在某些情况下,您可能希望使用 中间件 并根据权限重定向用户:
- 执行乐观检查。由于中间件在每个路由上运行,这是集中重定向逻辑和预过滤未授权用户的好方法
- 保护用户之间共享数据的静态路由(如付费内容)
但由于中间件在每个路由上运行(包括 预取 的路由),重要的是仅从 cookie 读取会话(乐观检查),避免数据库检查以防止性能问题。
例如:
import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
// 1. 指定受保护和公开路由
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
export default async function middleware(req: NextRequest) {
// 2. 检查当前路由是受保护还是公开
const path = req.nextUrl.pathname
const isProtectedRoute = protectedRoutes.includes(path)
const isPublicRoute = publicRoutes.includes(path)
// 3. 从 cookie 解密会话
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
// 4. 如果用户未认证,重定向到 /login
if (isProtectedRoute && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}
// 5. 如果用户已认证,重定向到 /dashboard
if (
isPublicRoute &&
session?.userId &&
!req.nextUrl.pathname.startsWith('/dashboard')
) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
}
return NextResponse.next()
}
// 中间件不应运行的路由
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. 指定受保护和公开路由
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
export default async function middleware(req) {
// 2. 检查当前路由是受保护还是公开
const path = req.nextUrl.pathname
const isProtectedRoute = protectedRoutes.includes(path)
const isPublicRoute = publicRoutes.includes(path)
// 3. 从 cookie 解密会话
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
// 5. 如果用户未认证,重定向到 /login
if (isProtectedRoute && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}
// 6. 如果用户已认证,重定向到 /dashboard
if (
isPublicRoute &&
session?.userId &&
!req.nextUrl.pathname.startsWith('/dashboard')
) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
}
return NextResponse.next()
}
// 中间件不应运行的路由
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
虽然中间件对初始检查很有用,但它不应该是保护数据的唯一防线。大多数安全检查应尽可能靠近数据源执行,详见 数据访问层。
提示:
- 在中间件中,也可以使用
req.cookies.get('session').value
读取 cookie- 中间件使用 Edge 运行时,请检查您的认证库和会话管理库是否兼容
- 可以使用中间件中的
matcher
属性指定中间件应运行的路由。但对于认证,建议中间件在所有路由上运行
创建数据访问层 (DAL)
保护 API 路由
Next.js 中的 API 路由对于处理服务器端逻辑和数据管理至关重要。必须保护这些路由,确保只有授权用户可以访问特定功能。这通常涉及验证用户的认证状态和基于角色的权限。
以下是保护 API 路由的示例:
import { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const session = await getSession(req)
// 检查用户是否已认证
if (!session) {
res.status(401).json({
error: '用户未认证',
})
return
}
// 检查用户是否具有 'admin' 角色
if (session.user.role !== 'admin') {
res.status(401).json({
error: '未经授权的访问:用户没有管理员权限。',
})
return
}
// 为授权用户继续执行路由
// ... API 路由的实现
}
export default async function handler(req, res) {
const session = await getSession(req)
// 检查用户是否已认证
if (!session) {
res.status(401).json({
error: '用户未认证',
})
return
}
// 检查用户是否具有 'admin' 角色
if (session.user.role !== 'admin') {
res.status(401).json({
error: '未经授权的访问:用户没有管理员权限。',
})
return
}
// 为授权用户继续执行路由
// ... API 路由的实现
}
此示例展示了一个具有两层安全检查的 API 路由,用于认证和授权。它首先检查活动会话,然后验证登录用户是否为 'admin'。这种方法确保仅限认证和授权用户安全访问,保持请求处理的强大安全性。
资源
现在您已了解 Next.js 中的认证,以下是帮助您实现安全认证和会话管理的 Next.js 兼容库和资源:
认证库
会话管理库
延伸阅读
要继续学习认证和安全,请查看以下资源: