在现代 Web 开发中,认证(Authentication) 与 授权(Authorization) 是构建安全应用的核心环节。Next.js 14 引入的 App Router 架构结合 React Server Components 与 Server Actions,为开发者提供了更安全、更高效的认证实现方式。
本文将带你从零搭建一个具备完整认证与授权能力的 Next.js 应用系统。
一、核心概念区分
在动手前,先厘清三个关键概念:
概念 | 说明 |
---|---|
认证(Authentication) | 验证"你是谁"------例如通过用户名/密码、OAuth 登录等方式确认用户身份。 |
会话管理(Session Management) | 在用户登录后,持续跟踪其身份状态,通常通过 Cookie 或 Token 实现。 |
授权(Authorization) | 决定"你能做什么"------例如普通用户只能查看自己的资料,管理员可删除内容。 |
二、整体架构设计
Next.js 推荐采用 服务端优先 的安全模型:
- 表单提交 → Server Action(服务端执行)
- 会话存储 → HttpOnly Cookie + JWT(或数据库)
- 权限控制 → Middleware(乐观检查) + Data Access Layer(安全检查)
⚠️ 安全原则:所有敏感操作必须在服务端验证身份与权限,客户端仅用于 UI 展示。
三、Step 1:实现用户注册与登录(认证)
1. 创建注册表单(Client Component)
tsx
// app/signup/page.tsx
'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">Name</label>
<input name="name" />
</div>
{state?.errors?.name && <p>{state.errors.name[0]}</p>}
<div>
<label htmlFor="email">Email</label>
<input name="email" type="email" />
</div>
{state?.errors?.email && <p>{state.errors.email[0]}</p>}
<div>
<label htmlFor="password">Password</label>
<input name="password" type="password" />
</div>
{state?.errors?.password && (
<ul>
{state.errors.password.map(err => <li key={err}>- {err}</li>)}
</ul>
)}
<button type="submit" disabled={pending}>
Sign Up
</button>
</form>
)
}
2. 定义表单验证(Zod Schema)
ts
// lib/definitions.ts
import { z } from 'zod'
export const SignupFormSchema = z.object({
name: z.string().min(2).trim(),
email: z.string().email().trim(),
password: z.string()
.min(8)
.regex(/[a-zA-Z]/)
.regex(/[0-9]/)
.regex(/[^a-zA-Z0-9]/)
})
export type FormState = {
errors?: {
name?: string[]
email?: string[]
password?: string[]
}
message?: string
}
3. 实现 Server Action(服务端逻辑)
ts
// app/actions/auth.ts
'use server'
import { SignupFormSchema, FormState } from '@/lib/definitions'
import { db } from '@/lib/db'
import { users } from '@/lib/schema'
import bcrypt from 'bcrypt'
import { createSession } from '@/lib/session'
import { redirect } from 'next/navigation'
export async function signup(
prevState: FormState,
formData: FormData
): Promise<FormState> {
// 1. 验证表单
const validated = SignupFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
})
if (!validated.success) {
return {
errors: validated.error.flatten().fieldErrors,
}
}
// 2. 检查邮箱是否已存在(可选)
const existing = await db.query.users.findFirst({
where: (users, { eq }) => eq(users.email, validated.data.email)
})
if (existing) {
return { message: 'Email already in use' }
}
// 3. 创建用户(密码哈希)
const hashed = await bcrypt.hash(validated.data.password, 10)
const [user] = await db
.insert(users)
.values({
name: validated.data.name,
email: validated.data.email,
password: hashed,
})
.returning({ id: users.id })
if (!user) {
return { message: 'Failed to create user' }
}
// 4. 创建会话
await createSession(user.id)
// 5. 跳转
redirect('/dashboard')
}
四、Step 2:会话管理(Session Management)
Next.js 推荐使用 无状态会话(Stateless Session),即通过 JWT 存储在 HttpOnly Cookie 中。
1. 生成密钥(.env.local)
bash
SESSION_SECRET=your_32_byte_base64_encoded_secret_here
生成命令:
openssl rand -base64 32
2. 加密/解密会话(使用 jose
)
ts
// lib/session.ts
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
const secret = new TextEncoder().encode(process.env.SESSION_SECRET)
export async function encrypt(payload: { userId: string }) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(secret)
}
export async function decrypt(session: string | undefined) {
try {
const { payload } = await jwtVerify(session || '', secret, {
algorithms: ['HS256'],
})
return payload as { userId: string }
} catch {
return null
}
}
3. 创建/删除会话
ts
// lib/session.ts(续)
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export async function createSession(userId: string) {
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const session = await encrypt({ userId })
;(await cookies()).set('session', session, {
httpOnly: true,
secure: true,
expires,
sameSite: 'lax',
path: '/',
})
}
export async function deleteSession() {
;(await cookies()).delete('session')
}
export async function logout() {
await deleteSession()
redirect('/login')
}
五、Step 3:授权控制(Authorization)
1. 中间件(Middleware):路由级保护
ts
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from './lib/session'
const protectedRoutes = ['/dashboard', '/profile']
const publicRoutes = ['/', '/login', '/signup']
export default async function middleware(req: NextRequest) {
const path = req.nextUrl.pathname
const isProtected = protectedRoutes.some(r => path.startsWith(r))
const isPublic = publicRoutes.includes(path)
const session = await decrypt((await req.cookies).get('session')?.value)
if (isProtected && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}
if (isPublic && session?.userId && !path.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
✅ Middleware 仅做乐观检查(基于 Cookie),不查数据库,保证性能。
2. 数据访问层(DAL):安全检查核心
ts
// lib/dal.ts
import 'server-only'
import { cache } from 'react'
import { cookies } from 'next/headers'
import { decrypt } from './session'
import { redirect } from 'next/navigation'
import { db } from './db'
import { users } from './schema'
export const verifySession = cache(async () => {
const session = await decrypt((await cookies()).get('session')?.value)
if (!session?.userId) {
redirect('/login')
}
return { userId: session.userId }
})
export const getUser = cache(async () => {
const { userId } = await verifySession()
const user = await db.query.users.findFirst({
where: (users, { eq }) => eq(users.id, userId),
columns: { id: true, name: true, email: true, role: true },
})
return user
})
3. 在 Server Component 中使用
tsx
// app/dashboard/page.tsx
import { getUser } from '@/lib/dal'
export default async function Dashboard() {
const user = await getUser() // 自动验证会话
return <div>Welcome, {user?.name}!</div>
}
4. 在 Server Action 中授权
ts
// app/actions/deletePost.ts
'use server'
import { verifySession } from '@/lib/dal'
import { db } from '@/lib/db'
export async function deletePost(postId: string) {
const session = await verifySession()
// 检查用户是否有权限(例如:是否为作者或管理员)
const post = await db.query.posts.findFirst({ where: ... })
if (post?.authorId !== session.userId && session.role !== 'admin') {
throw new Error('Unauthorized')
}
await db.delete(posts).where(...)
}
六、安全最佳实践
- 永远不在客户端存储敏感信息(如密码、完整用户对象)。
- Cookie 必须设置
HttpOnly
+Secure
+SameSite=Lax
。 - JWT payload 仅包含必要字段 (如
userId
,不含 email、phone 等)。 - 所有数据操作必须在服务端验证权限,即使前端已隐藏按钮。
- 优先使用认证库(如 Auth.js / NextAuth.js、Clerk、Supabase Auth)以减少安全风险。
七、推荐认证库(官方建议)
场景 | 推荐方案 |
---|---|
快速集成 | Auth.js(支持 OAuth、Credentials、Email 等) |
企业级 | Clerk、Supabase Auth |
自研需求 | 本文方案 + iron-session 或 jose |
💡 官方提示:自研认证系统复杂且易出错,除非必要,建议使用成熟库。
结语
Next.js 的 App Router 架构为认证授权提供了清晰、安全的实现路径。通过 Server Actions + HttpOnly Cookie + Middleware + DAL 的组合,我们既能保障安全性,又能享受服务端渲染与流式响应的性能优势。
记住:认证是入口,授权是守门人,而安全,永远是开发者的责任。