从0死磕全栈之Next.js 应用中的认证与授权:从零实现安全用户系统

在现代 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(...)
}

六、安全最佳实践

  1. 永远不在客户端存储敏感信息(如密码、完整用户对象)。
  2. Cookie 必须设置 HttpOnly + Secure + SameSite=Lax
  3. JWT payload 仅包含必要字段 (如 userId,不含 email、phone 等)。
  4. 所有数据操作必须在服务端验证权限,即使前端已隐藏按钮。
  5. 优先使用认证库(如 Auth.js / NextAuth.js、Clerk、Supabase Auth)以减少安全风险。

七、推荐认证库(官方建议)

场景 推荐方案
快速集成 Auth.js(支持 OAuth、Credentials、Email 等)
企业级 ClerkSupabase Auth
自研需求 本文方案 + iron-sessionjose

💡 官方提示:自研认证系统复杂且易出错,除非必要,建议使用成熟库


结语

Next.js 的 App Router 架构为认证授权提供了清晰、安全的实现路径。通过 Server Actions + HttpOnly Cookie + Middleware + DAL 的组合,我们既能保障安全性,又能享受服务端渲染与流式响应的性能优势。

记住:认证是入口,授权是守门人,而安全,永远是开发者的责任。

相关推荐
苏打水com3 小时前
携程前端业务:在线旅游生态下的「复杂行程交互」与「高并发预订」实践
前端·状态模式·旅游
Darenm1113 小时前
深入理解CSS BFC:块级格式化上下文
前端·css
Darenm1113 小时前
JavaScript事件流:冒泡与捕获的深度解析
开发语言·前端·javascript
@大迁世界3 小时前
第03章: Vue 3 组合式函数深度指南
前端·javascript·vue.js·前端框架·ecmascript
小白64023 小时前
前端梳理体系从常问问题去完善-框架篇(react生态)
前端·css·html·reactjs
Hy行者勇哥3 小时前
数据中台的数据源与数据处理流程
大数据·前端·人工智能·学习·个人开发
JarvanMo3 小时前
Riverpod 3.0 关键变化与实战用法
前端
二十雨辰4 小时前
vite与ts的结合
开发语言·前端·vue.js
我是日安4 小时前
从零到一打造 Vue3 响应式系统 Day 25 - Watch:清理 SideEffect
前端·javascript·vue.js