Chapter 07

认证系统 Auth.js v5

OAuth 登录、凭证认证、Session 管理与路由保护的完整实现

7.1 Auth.js v5 概述

Auth.js(前身为 NextAuth.js)是 Next.js 生态中最成熟的认证库,v5 版本专为 App Router 重写,支持 Next.js 15 和 React 19。它提供了开箱即用的 OAuth(GitHub、Google 等)、邮箱/密码、魔术链接等多种认证方式,同时内置了 JWT 和数据库 Session 两种 Session 策略。

7.2 安装与基础配置

# 安装 Auth.js v5 和 Prisma Adapter
pnpm add next-auth@beta @auth/prisma-adapter
SHELL
// auth.ts — Auth.js 核心配置(项目根目录)
import NextAuth from 'next-auth'
import GitHub  from 'next-auth/providers/github'
import Google  from 'next-auth/providers/google'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { db } from '@/lib/db'

export const {
  handlers,    // GET/POST Route Handler
  signIn,      // 服务端调用登录
  signOut,     // 服务端调用注销
  auth,        // 获取当前 Session
} = NextAuth({
  adapter: PrismaAdapter(db),

  providers: [
    GitHub({
      clientId:     process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
    Google({
      clientId:     process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],

  session: { strategy: 'jwt' }, // 或 'database'

  callbacks: {
    // 将 user.id 注入 JWT token
    jwt({ token, user }) {
      if (user) token.id = user.id
      return token
    },
    // 将 user.id 暴露给 session
    session({ session, token }) {
      session.user.id = token.id as string
      return session
    },
  },

  pages: {
    signIn: '/login',    // 自定义登录页
    error:  '/auth/error' // 自定义错误页
  },
})
TS
// app/api/auth/[...nextauth]/route.ts — 必须创建此文件
export { handlers as GET, handlers as POST } from '@/auth'
TS

7.3 Credentials 认证(用户名/密码)

Credentials Provider 允许自定义认证逻辑,适用于已有用户名/密码系统的场景。需要注意:Credentials 认证不支持数据库 Session(因为安全考虑),必须使用 JWT Strategy。

import Credentials from 'next-auth/providers/credentials'
import bcrypt from 'bcryptjs'
import { z } from 'zod'

const LoginSchema = z.object({
  email:    z.string().email(),
  password: z.string().min(8),
})

// 在 providers 数组中添加
Credentials({
  async authorize(credentials) {
    const parsed = LoginSchema.safeParse(credentials)
    if (!parsed.success) return null

    const { email, password } = parsed.data

    const user = await db.user.findUnique({
      where: { email }
    })

    if (!user || !user.password) return null

    // bcrypt 对比密码哈希
    const passwordMatch = await bcrypt.compare(
      password,
      user.password
    )

    if (!passwordMatch) return null

    // 返回用户对象(不要包含密码)
    return { id: user.id, email: user.email, name: user.name }
  }
})
TS
🚨

密码存储安全永远不要以明文存储密码。注册时使用 bcrypt.hash(password, 12) 生成哈希(数字越大越安全,12是推荐值)。永远不要将哈希密码包含在 session 或响应中。

7.4 在不同位置获取 Session

Auth.js v5 提供统一的 auth() 函数,可以在 Server Component、API Route、Server Action 和 middleware 中获取当前用户 Session。

// 1. Server Component(最常用)
import { auth } from '@/auth'

export default async function ProfilePage() {
  const session = await auth()

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

  return <p>你好,{session.user.name}</p>
}

// 2. Server Action
'use server'
export async function createPost(formData: FormData) {
  const session = await auth()
  if (!session?.user) throw new Error('Unauthorized')

  await db.post.create({
    data: { ...data, authorId: session.user.id }
  })
}

// 3. Client Component — 使用 useSession
'use client'
import { useSession } from 'next-auth/react'

function UserMenu() {
  const { data: session, status } = useSession()
  if (status === 'loading') return <p>加载中</p>
  if (!session) return <a href="/login">登录</a>
  return <p>{session.user.name}</p>
}
TSX

7.5 Middleware 路由保护

使用 middleware.ts 可以在请求到达页面之前进行认证检查,是保护整批路由最高效的方式(在 Edge 上运行,无需启动 Node.js 进程)。

// middleware.ts(项目根目录)
export { auth as default } from '@/auth'

// 或者自定义逻辑:
import { auth } from '@/auth'
import { NextResponse } from 'next/server'

export default auth((req) => {
  const { nextUrl } = req
  const isLoggedIn = !!req.auth

  const isProtected = nextUrl.pathname.startsWith('/dashboard')

  if (isProtected && !isLoggedIn) {
    const redirectUrl = new URL('/login', nextUrl)
    redirectUrl.searchParams.set('callbackUrl', nextUrl.pathname)
    return NextResponse.redirect(redirectUrl)
  }
})

export const config = {
  // 匹配所有路径,排除静态资源和 API
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}
TS

7.6 登录页与登录按钮

// app/login/page.tsx — 自定义登录页
import { signIn } from '@/auth'

export default function LoginPage() {
  return (
    <div className="login-page">
      <h1>登录</h1>

      {/* GitHub OAuth 登录 */}
      <form
        action={async () => {
          'use server'
          await signIn('github', { redirectTo: '/dashboard' })
        }}
      >
        <button type="submit">使用 GitHub 登录</button>
      </form>

      {/* Google OAuth 登录 */}
      <form
        action={async () => {
          'use server'
          await signIn('google', { redirectTo: '/dashboard' })
        }}
      >
        <button type="submit">使用 Google 登录</button>
      </form>
    </div>
  )
}

// 退出登录按钮
import { signOut } from '@/auth'

function SignOutButton() {
  return (
    <form
      action={async () => {
        'use server'
        await signOut({ redirectTo: '/' })
      }}
    >
      <button type="submit">退出登录</button>
    </form>
  )
}
TSX
💡

SessionProvider 配置如果需要在 Client Component 中使用 useSession,需要在根布局的 Providers 中添加 <SessionProvider>(来自 next-auth/react)。Server Component 直接调用 auth() 无需 Provider。