首页 / Next.js 16 入门教程 / 认证方案

Next.js 16 入门教程

认证方案

本教程共 42 篇 · 第 25 篇 · 更新于 2026-07-30 · 约 8 分钟阅读

Next.jsNext.js 16 入门教程认证SessionJWT授权

25. 认证方案

本节目标:理解认证的三个核心概念(认证、会话管理、授权),掌握基于 Cookie 的无状态会话和基于数据库的会话两种实现方式,了解主流认证库的特点与选择。

认证的三个核心概念

在实现认证之前,需要将整个过程拆解为三个独立的概念:

  1. 认证(Authentication):验证用户身份,确认”你是你所说的那个人”。通常需要用户提供用户名和密码等凭证。
  2. 会话管理(Session Management):在多次请求之间追踪用户的认证状态。
  3. 授权(Authorization):决定用户可以访问哪些路由和数据。
Tip

虽然可以自己实现完整的认证方案,但出于安全性和简洁性考虑,推荐使用成熟的认证库。它们提供认证、会话管理、社交登录、多因素认证、角色访问控制等开箱即用的功能。

认证流程:注册与登录

第一步:捕获用户凭证

使用 React 的 <form> 元素配合 Server Action 来捕获用户凭证。由于 Server Action 始终在服务器上执行,这为处理认证逻辑提供了安全的环境。

// app/ui/signup-form.tsx
import { signup } from '@/app/actions/auth'

export function SignupForm() {
  return (
    <form action={signup}>
      <div>
        <label htmlFor="name">姓名</label>
        <input id="name" name="name" placeholder="请输入姓名" />
      </div>
      <div>
        <label htmlFor="email">邮箱</label>
        <input id="email" name="email" type="email" placeholder="请输入邮箱" />
      </div>
      <div>
        <label htmlFor="password">密码</label>
        <input id="password" name="password" type="password" />
      </div>
      <button type="submit">注册</button>
    </form>
  )
}

第二步:服务器端表单验证

使用 Zod 等 schema 验证库在服务器端验证表单字段:

// app/lib/definitions.ts
import * as z from 'zod'

export const SignupFormSchema = z.object({
  name: z
    .string()
    .min(2, { error: '姓名至少需要 2 个字符' })
    .trim(),
  email: z.email({ error: '请输入有效的邮箱' }).trim(),
  password: z
    .string()
    .min(8, { error: '密码至少需要 8 个字符' })
    .regex(/[a-zA-Z]/, { error: '密码必须包含至少一个字母' })
    .regex(/[0-9]/, { error: '密码必须包含至少一个数字' })
    .regex(/[^a-zA-Z0-9]/, {
      error: '密码必须包含至少一个特殊字符',
    })
    .trim(),
})

export type FormState =
  | {
      errors?: {
        name?: string[]
        email?: string[]
        password?: string[]
      }
      message?: string
    }
  | undefined

在 Server Action 中使用:

// app/actions/auth.ts
import { SignupFormSchema, FormState } from '@/app/lib/definitions'

export async function signup(state: FormState, formData: FormData) {
  // 验证表单字段
  const validatedFields = SignupFormSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    password: formData.get('password'),
  })

  // 如果验证失败,提前返回错误
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }

  // 调用认证库或数据库创建用户...
}

第三步:创建用户或验证凭证

验证通过后,可以创建新用户或验证现有用户:

// app/actions/auth.ts
export async function signup(state: FormState, formData: FormData) {
  // ... 验证步骤 ...

  const { name, email, password } = validatedFields.data

  // 密码加密存储
  const hashedPassword = await bcrypt.hash(password, 10)

  // 插入数据库
  const data = await db
    .insert(users)
    .values({
      name,
      email,
      password: hashedPassword,
    })
    .returning({ id: users.id })

  const user = data[0]

  if (!user) {
    return {
      message: '创建账户时发生错误',
    }
  }

  // TODO: 创建会话、重定向
}

会话管理

会话管理确保用户的认证状态在多次请求之间保持。有两种主要方式:

无状态会话(Stateless Sessions)

会话数据存储在浏览器的 Cookie 中,每次请求都会携带 Cookie,服务器据此验证会话。

实现步骤:

  1. 生成密钥
openssl rand -base64 32

将生成的密钥存入环境变量:

# .env
SESSION_SECRET=your_secret_key
  1. 加密/解密会话
// app/lib/session.ts
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: 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('会话验证失败')
  }
}
  1. 设置 Cookie
// app/lib/session.ts
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 })
  const cookieStore = await cookies()

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

Cookie 应该在服务器端设置,防止客户端篡改。推荐的安全选项:

  • HttpOnly:阻止客户端 JavaScript 访问
  • Secure:仅通过 HTTPS 传输
  • SameSite:控制跨站请求行为

数据库会话(Database Sessions)

会话数据存储在数据库中,浏览器只持有加密的会话 ID。这种方式更安全,但实现更复杂。

// app/lib/session.ts
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. 在数据库中创建会话
  const data = await db
    .insert(sessions)
    .values({
      userId: id,
      expiresAt,
    })
    .returning({ id: sessions.id })

  const sessionId = data[0].id

  // 2. 加密会话 ID
  const session = await encrypt({ sessionId, expiresAt })

  // 3. 在 Cookie 中存储会话
  const cookieStore = await cookies()
  cookieStore.set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: 'lax',
    path: '/',
  })
}

会话刷新与删除

刷新会话(延长过期时间):

export async function updateSession() {
  const session = (await 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)

  const cookieStore = await cookies()
  cookieStore.set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expires,
    sameSite: 'lax',
    path: '/',
  })
}

删除会话(用户登出):

export async function deleteSession() {
  const cookieStore = await cookies()
  cookieStore.delete('session')
}

授权控制

认证完成后,需要实现授权来控制用户可以访问什么资源。

创建数据访问层(DAL)

推荐创建 DAL 来集中管理数据请求和授权逻辑:

// app/lib/dal.ts
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
import { cache } from 'react'

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

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

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

使用 DAL 获取用户数据:

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),
      // 只返回需要的列,而不是整个用户对象
      columns: {
        id: true,
        name: true,
        email: true,
      },
    })

    return data[0]
  } catch (error) {
    console.log('获取用户失败')
    return null
  }
})

使用数据传输对象(DTO)

只返回必要的数据,避免暴露敏感信息:

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

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

  const currentUser = await getUser(user.id)

  // 只返回查询所需的数据
  return {
    username: canSeeUsername(currentUser) ? user.username : null,
    phonenumber: canSeePhoneNumber(currentUser, user.team)
      ? user.phonenumber
      : null,
  }
}

在 Server Components 中检查授权

// app/dashboard/page.tsx
import { verifySession } from '@/app/lib/dal'

export default async function Dashboard() {
  const session = await verifySession()
  const userRole = session?.user?.role

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

在 Layout 中做认证检查要谨慎,因为 Layout 不会在导航时重新渲染。应该在数据源附近或条件渲染的组件中进行检查。

认证库推荐

特点适用场景
NextAuth.js (Auth.js)开源、灵活、支持多种提供商需要自定义认证流程
Clerk开箱即用、UI 组件丰富快速集成、付费服务
Auth0企业级、功能全面大型企业应用
Supabase Auth与 Supabase 生态集成使用 Supabase 数据库
Better Auth现代、TypeScript 优先新项目、追求类型安全

会话管理库推荐

特点
Iron Session轻量级、无状态、基于 Cookie
JoseJWT 实现、支持 Edge Runtime

小结

这一章我们学了认证的完整流程:

  1. 认证、会话管理、授权是三件不同的事
  2. 认证逻辑放服务端,用 Server Action 处理
  3. 无状态会话简单,数据库会话更安全
  4. 用 DAL 和 DTO 集中管理授权逻辑
  5. 认证检查要靠近数据源,别只在 Layout 里做