从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 的组合,我们既能保障安全性,又能享受服务端渲染与流式响应的性能优势。

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

相关推荐
恋猫de小郭20 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅1 天前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60611 天前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 天前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 天前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 天前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 天前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 天前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅1 天前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊1 天前
jwt介绍
前端