Next.js: 认证与授权

最后更新:2026-08-26

给全栈应用加上认证,就像给办公楼装上门禁系统——谁可以进、进到哪一层,都需要精确控制。

1. 你将学到


2. 一个全栈开发者的真实故事

(1) 痛点:上线前才发现没有认证

Alice 在一家 15 人的 SaaS 创业公司工作,她用 Next.js 16 开发了团队协作平台 "TaskFlow",功能齐全。但在上线前安全审计时,技术主管 Bob 指出:

"你的 /dashboard 页面任何人都能访问;API 端点没有 Token 验证;用户数据明文暴露。"

审计报告列出了以下问题:

问题 影响范围 风险等级
无登录页面 全部路由 🔴 高
API Route 未校验身份 /api/projects/* 🔴 高
无角色区分 所有用户看到管理面板 🟡 中
Session 无过期 登录一次永久有效 🔴 高

(2) Auth.js + Clerk 的解法

用 Auth.js v5 实现标准认证流程,Clerk 作为零配置备选方案。

TS
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import Google from 'next-auth/providers/google'
import Credentials from 'next-auth/providers/credentials'

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    GitHub,
    Google,
    Credentials({
      credentials: { email: {}, password: {} },
      async authorize(credentials) {
        const user = { id: '1', name: 'Alice', email: 'alice@taskflow.io', role: 'admin' }
        return credentials.email === 'alice@taskflow.io' && credentials.password === 'pass123' ? user : null
      }
    })
  ],
  callbacks: { session: ({ session, token }) => ({ ...session, user: { ...session.user, role: token.role } }) }
})

(3) 收益

维度 实施前 实施后
未登录访问 /dashboard 可访问 重定向到登录页
API 端点安全性 无校验 getToken() 验证 JWT
角色控制 admin / editor / viewer 三级
Session 有效期 永久 30 天自动过期
集成耗时 Auth.js: 2h / Clerk: 30min

3. Auth.js v5(NextAuth)Provider 集成

(1) Provider 架构

Auth.js v5 的 Provider 抽象层允许你通过统一接口对接多种认证源:

100%
graph TB
    A[请求认证] --> B{NextAuth Route Handler}
    B --> C[Credentials<br/>邮箱+密码]
    B --> D[OAuth<br/>GitHub / Google]
    B --> E[其他 Provider<br/>Auth0 / Azure AD]
    C --> F[JWT Callback<br/>token + session]
    D --> F
    E --> F
    F --> G[Session 返回客户端]
    G --> H[Middleware 校验]
    H --> I[受保护页面]
    H --> J[公开页面]

    style B fill:#cce5ff
    style F fill:#d4edda
Provider 类型 实现复杂度 用户体验 适用场景
Credentials 中(需自建登录页) 标准表单 自有账号体系
GitHub OAuth 一键登录 开发者工具
Google OAuth 一键登录 面向大众用户
OIDC / SAML 企业 SSO 企业内网

(2) 安装与初始化

BASH
npm install next-auth@beta
npx auth secret    # 生成 AUTH_SECRET
TS
// auth.ts — 集中认证配置
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import Google from 'next-auth/providers/google'

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    GitHub({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET! }),
    Google({ clientId: process.env.GOOGLE_ID!, clientSecret: process.env.GOOGLE_SECRET! })
  ]
})

▶ 示例:Credentials Provider 完整配置

TS
// auth.ts — Credentials + OAuth 混合
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import GitHub from 'next-auth/providers/github'

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Credentials({
      name: 'credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' }
      },
      async authorize(credentials) {
        const { email, password } = credentials as { email: string; password: string }
        // 实际项目查询数据库
        const user = { id: '1', name: 'Alice', email, role: 'admin' }
        if (email === 'admin@taskflow.io' && password === 'admin123') return user
        return null
      }
    }),
    GitHub
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) token.role = (user as any).role
      return token
    },
    async session({ session, token }) {
      session.user.role = token.role as string
      return session
    }
  }
})
💡 提示: authorize 函数返回 null 表示登录失败,Auth.js 自动返回 401。


4. Session 管理:JWT vs Database

(1) 两种策略对比

Auth.js v5 支持两种 Session 存储策略:

维度 JWT(默认) Database
存储位置 Cookie 中加密 JWT 数据库 Session
查询开销 零(无 DB 查询) 每次请求查一次 DB
失效即时性 依赖 JWT 过期时间 可即时撤销
扩展性 无需数据库 需 Prisma 等 ORM
适用规模 中小型应用 大型企业应用
100%
graph LR
    subgraph JWT 模式
        A1[登录] --> B1[生成 JWT<br/>含 user + role]
        B1 --> C1[写入 Cookie]
        C1 --> D1[请求 → middleware<br/>解密 JWT → 校验]
    end
    subgraph Database 模式
        A2[登录] --> B2[创建 Session<br/>写入 DB]
        B2 --> C2[Session ID → Cookie]
        C2 --> D2[请求 → middleware<br/>查询 DB → 校验]
    end

    style A1 fill:#d4edda
    style A2 fill:#cce5ff

(2) Database Session 配置

TS
// auth.ts — Database Session + Prisma
import NextAuth from 'next-auth'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@/lib/prisma'

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: { strategy: 'database' },
  providers: [GitHub, Google]
})

▶ 示例:获取 Session 并展示用户信息

TSX
// app/dashboard/page.tsx — 服务端获取 Session
import { auth } from '@/auth'

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

  if (!session?.user) return <p>请先登录</p>

  return (
    <div>
      <h1>欢迎回来,{session.user.name}</h1>
      <p>邮箱:{session.user.email}</p>
      <p>角色:{session.user.role}</p>
      <img src={session.user.image!} alt="avatar" width={48} height={48} />
    </div>
  )
}
💻 输出:

TEXT 📖 仅展示
<h1>欢迎回来,Alice</h1>
<p>邮箱:alice@taskflow.io</p>
<p>角色:admin</p>

5. Middleware 路由保护

(1) matcher 配置模式

middleware.ts 在请求到达页面之前拦截,检查 Session 有效性:

100%
graph TB
    A[用户请求 /dashboard/*] --> B{middleware.ts}
    B -->|有有效 Session| C[放行 → page.tsx]
    B -->|无 Session| D[重定向 /login]
    D --> E{公共路径?}
    E -->|/api/auth/*| F[放行]
    E -->|/_next/*| F
    E -->|/favicon.ico| F

    style B fill:#fff3cd
    style C fill:#d4edda
    style D fill:#f8d7da
matcher 模式 匹配路径 说明
/dashboard/:path* /dashboard/* 保护仪表盘
/api/projects/:path* /api/projects/* 保护 API
`/((?!auth _next favicon).*)`

(2) 完整 Middleware 实现

TS
// middleware.ts
import { auth } from '@/auth'
import { NextResponse } from 'next/server'

export default auth((req) => {
  const { pathname } = req.nextUrl
  const isLoggedIn = !!req.auth
  const isPublicPath = pathname.startsWith('/login') ||
    pathname.startsWith('/register') ||
    pathname.startsWith('/api/auth')

  if (!isLoggedIn && !isPublicPath) {
    return NextResponse.redirect(new URL('/login', req.url))
  }

  // RBAC:阻止非 admin 访问 /admin
  if (pathname.startsWith('/admin') && req.auth?.user?.role !== 'admin') {
    return NextResponse.redirect(new URL('/dashboard', req.url))
  }

  return NextResponse.next()
})

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|images).*)']
}

▶ 示例:API Route 加入认证保护

TS
// app/api/projects/route.ts — 受保护 API
import { getToken } from 'next-auth/jwt'
import { NextRequest, NextResponse } from 'next/server'

export async function GET(req: NextRequest) {
  const token = await getToken({ req })

  if (!token) {
    return NextResponse.json({ error: '未登录' }, { status: 401 })
  }

  // token.role 来自 JWT callback
  if (token.role !== 'admin' && token.role !== 'editor') {
    return NextResponse.json({ error: '权限不足' }, { status: 403 })
  }

  return NextResponse.json({ projects: [{ id: 1, name: 'TaskFlow' }] })
}
🔥 易错: getToken() 需配置 AUTH_SECRET 环境变量,否则返回 null


6. Clerk 第三方认证

(1) Clerk 特性对比

特性 Auth.js Clerk
安装复杂度 中(需配置 Provider) 低(npm + 环境变量)
UI 组件 自建登录页 <SignIn /> / <SignUp /> 开箱即用
多因素认证 需手动集成 内置支持
免费层限制 5,000 MAU 免费
自定义 UI 完全自由 限制较多

(2) Clerk 集成步骤

BASH
npm install @clerk/nextjs
# 在 .env.local 添加:
# NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=...
# CLERK_SECRET_KEY=...
TSX
// app/layout.tsx — ClerkProvider 包裹根布局
import { ClerkProvider } from '@clerk/nextjs'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider>
      <html lang="zh">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  )
}

▶ 示例:Clerk 登录页 + 受保护页面

TSX
// app/page.tsx — 登录页
import { SignIn, SignedIn, SignedOut, UserButton } from '@clerk/nextjs'

export default function HomePage() {
  return (
    <div>
      <SignedOut>
        <SignIn routing="hash" />
      </SignedOut>
      <SignedIn>
        <div>
          <UserButton afterSignOutUrl="/" />
          <h1>欢迎使用 TaskFlow</h1>
        </div>
      </SignedIn>
    </div>
  )
}
TSX
// app/dashboard/page.tsx — Clerk auth() 保护
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const { userId } = auth()

  if (!userId) redirect('/sign-in')

  return <div>Dashboard — 用户ID: {userId}</div>
}
💡 提示: Clerk 的 auth() 是服务端函数,在 Server Component 中使用不需要 'use client'

(3) Clerk Middleware

TS
// middleware.ts
import { clerkMiddleware } from '@clerk/nextjs/server'

export default clerkMiddleware()

export const config = {
  matcher: ['/((?!_next|sign-in|sign-up|favicon.ico).*)']
}

7. RBAC 角色权限控制

(1) 角色模型设计

100%
graph TB
    A[用户] --> B{Role}
    B --> C[admin<br/>全部权限]
    B --> D[editor<br/>读写项目]
    B --> E[viewer<br/>只读]

    C --> F[创建/删除项目]
    C --> G[管理成员]
    C --> H[修改设置]
    D --> I[编辑任务]
    D --> J[添加评论]
    E --> K[查看仪表盘]
    E --> L[阅读文档]

    style C fill:#d4edda
    style D fill:#cce5ff
    style E fill:#f8d7da
角色 权限值 可访问页面
admin 100 全部(含 /admin
editor 50 /dashboard/projects(可编辑)
viewer 20 /dashboard(只读)

▶ 示例:RBAC 权限检查组件

TSX
// components/PermissionGuard.tsx
import { auth } from '@/auth'
import { redirect } from 'next/navigation'

type Role = 'admin' | 'editor' | 'viewer'

const roleHierarchy: Record<Role, number> = { admin: 100, editor: 50, viewer: 20 }

export async function PermissionGuard({
  children,
  minRole
}: {
  children: React.ReactNode
  minRole: Role
}) {
  const session = await auth()
  const userRole = (session?.user?.role as Role) || 'viewer'

  if (roleHierarchy[userRole] < roleHierarchy[minRole]) {
    redirect('/dashboard')
  }

  return <>{children}</>
}
TSX
// app/admin/page.tsx — 使用 PermissionGuard
import { PermissionGuard } from '@/components/PermissionGuard'

export default function AdminPage() {
  return (
    <PermissionGuard minRole="admin">
      <h1>管理控制台</h1>
      <p>此页面仅 admin 可见。</p>
    </PermissionGuard>
  )
}

8. 完整示例:多 Provider 认证 + RBAC 综合实现

TSX
// app/dashboard/layout.tsx — 受保护布局 + RBAC
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
import { PermissionGuard } from '@/components/PermissionGuard'

export default async function DashboardLayout({
  children,
  analytics,
  team
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  const session = await auth()

  if (!session) redirect('/login')

  const user = session.user!

  return (
    <div>
      <header>
        <h1>TaskFlow</h1>
        <p>欢迎, {user.name} ({user.role})</p>
        <nav>
          <a href="/dashboard">总览</a>
          {user.role === 'admin' && <a href="/admin">管理</a>}
          <a href="/api/auth/signout">退出</a>
        </nav>
      </header>
      <div style={{ display: 'flex', gap: '2rem' }}>
        <main>{children}</main>
        <aside>
          {analytics}
          <PermissionGuard minRole="editor">{team}</PermissionGuard>
        </aside>
      </div>
    </div>
  )
}
TS
// app/api/projects/[id]/route.ts — 完整 API 保护
import { getToken } from 'next-auth/jwt'
import { NextRequest, NextResponse } from 'next/server'

const roles = { admin: 100, editor: 50, viewer: 20 }

export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const token = await getToken({ req })
  const { id } = await params

  if (!token) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
  if (roles[token.role as keyof typeof roles] < 50) {
    return NextResponse.json({ error: 'forbidden' }, { status: 403 })
  }

  // 实际删除项目
  return NextResponse.json({ id, deleted: true })
}
💻 输出(未登录访问 DELETE):

JSON
{ "error": "unauthorized" }
💻 输出(viewer 角色访问 DELETE):

JSON
{ "error": "forbidden" }

❓ 常见问题

Q Auth.js v4(NextAuth)和 v5 有什么区别?
A v5 使用 App Router 的 Route Handler(app/api/auth/[...nextauth]/route.ts),支持 Server Components 中直接 auth() 获取 Session,不再需要 getSession()SessionProvider
Q JWT Session 和 Database Session 应该选哪个?
A 小应用或不需要即时撤销 Session 选 JWT(零 DB 查询)。企业应用需要即时踢人、多设备管理的选 Database。JWT 模式下,撤销 Session 要等到 JWT 过期(默认 30 天)。
Q Clerk 和 Auth.js 什么场景选哪个?
A 需要快速上线、不想自建 UI 的 → Clerk(30 分钟集成)。需要完全自定义 UI、自建账号体系的 → Auth.js。Clerk 免费层上限 5,000 MAU,超出后 €25/月。
Q Middleware 里 auth()getToken() 有什么区别?
A auth() 是 Auth.js v5 的包装函数,返回完整的 Session 对象。getToken() 来自 next-auth/jwt,只解析 JWT Token,性能更好。在 Middleware 中推荐 auth() 简洁;在 API Route 中推荐 getToken() 轻量。
Q 如何保护静态导出的页面?
A 使用 output: 'export' 的静态站点无法使用 Middleware(需要 Node.js 运行时)。此时应在 Client Component 中用 useSession() Hook 检查登录状态,或用 Clerk 的 <SignedIn> / <SignedOut> 条件渲染。

📖 小节


📝 作业

  1. 基础题(⭐):创建一个新的 Next.js 项目,集成 Auth.js v5 的 GitHub Provider,在 /dashboard 页面展示用户头像和邮箱。

  2. 进阶题(⭐⭐):在 Middleware 中添加 RBAC 逻辑:admin 可访问 /admin/*editor 可访问 /projects/* 编辑接口,其他角色越权时返回 403 页面。

  3. 挑战题(⭐⭐⭐):混合使用 Auth.js(Credentials Provider)和 Prisma:用户在登录页输入邮箱密码 → authorize 查询数据库校验 → JWT 中写入 roleorgId → Middleware 根据 orgId 路由到对应组织的工作区。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏