切换语言
切换主题

Next.js 路由保护与权限控制:Middleware 与多层防护完整指南

上个月给客户做安全审查时,看着我写的权限控制代码,审查员皱着眉头问:“你就这?只在 Middleware 里拦了一下?”

我当时挺不服气的。Middleware 不是专门干这个的吗?未登录的用户直接重定向到登录页,已登录的放行。看起来滴水不漏啊。

审查员打开浏览器开发者工具,直接给我演示了一把:绕过前端路由,用 Postman 直接调后台 API,轻松拿到了本该被保护的数据。

那一刻有点懵。

后来研究了一圈才明白:Next.js 的权限控制,从来就不是单靠 Middleware 能搞定的。你得有一套完整的多层防护架构。

这篇文章就来聊聊:Middleware 该怎么用?getServerSession 和它是什么关系?管理后台的 RBAC 权限体系到底该怎么设计?还有那些能直接拿去用的代码模板。

如果你正在做 Next.js 的权限控制,或者对 Middleware 和 getServerSession 的配合有疑惑,这篇文章应该能帮到你。

权限控制为什么不能只靠 Middleware

Middleware 到底是干嘛的

先说清楚 Middleware 的定位。

它运行在 Edge Runtime 上,是用户请求进来后最早接触到的那一层。你可以在这里做一些”粗筛”的工作:检查用户有没有登录,是管理员还是普通用户,该放行还是重定向。

速度快,位置早。听起来很适合做权限控制对吧?

确实适合。但问题是,只靠它不够。

一个真实的漏洞案例

今年 1 月份,安全研究人员披露了 CVE-2025-29927 这个漏洞。简单说就是:攻击者可以通过在请求头里加一个特殊的 x-middleware-subrequest 字段,直接绕过 Middleware 的检查。

你在 Middleware 里写的所有逻辑,在这种攻击下形同虚设。

这不是个例。Middleware 作为最外层的防线,它本身就可能被绕过、被误配置、或者因为边缘环境的限制无法做复杂的权限判断。

Next.js 官方文档也特别强调了这一点:“While Middleware can be useful for initial checks, it should not be your only line of defense.”

翻译过来就是:别把宝全压在 Middleware 上。

前端拦住了,后端呢?

我之前做过一个管理后台项目。在 Middleware 里写了登录检查,未登录的用户访问 /admin 路由会被重定向到登录页。

看起来很安全。用户点击菜单,跳转路由,都会经过 Middleware 的检查。

问题出在哪?API 没防。

有个测试同事好奇心比较重,打开 Network 面板看了看,发现删除用户的接口是 POST /api/users/delete。他直接用 curl 调了一下这个接口,参数随便填。

删成功了。

因为 API Route 里根本没有权限检查。我只在 Middleware 里拦了前端路由,完全没考虑到有人会直接调接口。

这就是只依赖 Middleware 的问题:它只能管住前端的门,管不住后端的窗。

Next.js 官方推荐的做法

官方文档里提到了一个原则:“Proximity Principle”——把权限检查放在离数据最近的地方。

什么意思?

数据在数据库里。你要保护数据,就应该在访问数据库之前做权限检查。不是在路由层、不是在页面层,而是在数据层。

当然,这不是说 Middleware 没用。Middleware 可以做第一层拦截:未登录的重定向,明显没权限的角色直接拒绝。但光有这一层不够,你还需要:

  • Server Component 里的检查:页面渲染前,验证用户是否有权限访问这个页面
  • API Route 和 Server Action 里的检查:每个数据操作前,再次验证权限
  • 甚至在数据库查询层的检查:通过 Row-Level Security 或查询过滤,确保用户只能访问自己有权限的数据

多层防护。一层被绕过了,还有下一层。

说实话,一开始我也觉得这样太麻烦了。但后来踩了坑才知道,安全这事儿,你嫌麻烦的地方正是攻击者感兴趣的地方。

Middleware 和 getServerSession 的正确配合

为什么 Middleware 里不能用 getServerSession

刚开始用 NextAuth 的时候,我也被这个问题困扰过。

看文档,在 Server Component 里获取 session 要用 getServerSession(authOptions)。很自然地,我就想在 Middleware 里也这么用。

结果报错了。

查了半天才搞明白:Middleware 运行在 Edge Runtime,而 getServerSession 需要 Node.js Runtime。两者不兼容。

Edge Runtime 是 Vercel 搞的一个轻量级运行环境,没有 Node.js 的完整 API,但速度快、全球分布。Middleware 为了性能选择了 Edge Runtime,但代价就是你不能用所有 Node.js 的东西。

那在 Middleware 里怎么获取 session?

正确的做法:用 getToken 或 withAuth

NextAuth 提供了两个专门给 Middleware 用的 API:

方式一:使用 getToken

// middleware.ts
import { getToken } from "next-auth/jwt"
import { NextResponse } from "next/server"

export async function middleware(req) {
  const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET })
  
  if (!token) {
    return NextResponse.redirect(new URL('/login', req.url))
  }
  
  // 检查角色
  if (req.nextUrl.pathname.startsWith('/admin') && token.role !== 'admin') {
    return NextResponse.redirect(new URL('/403', req.url))
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: ['/admin/:path*', '/dashboard/:path*']
}

getToken 会从请求的 cookie 中解析 JWT token,拿到用户信息。注意它只支持 JWT session 策略,如果你用的是 database session,这招不行。

方式二:使用 withAuth 高阶函数

// middleware.ts
import { withAuth } from "next-auth/middleware"

export default withAuth({
  callbacks: {
    authorized: ({ token, req }) => {
      // 未登录
      if (!token) return false
      
      // admin 路由只允许 admin 角色访问
      if (req.nextUrl.pathname.startsWith('/admin')) {
        return token.role === 'admin'
      }
      
      return true
    }
  }
})

export const config = {
  matcher: ['/admin/:path*', '/dashboard/:path*']
}

withAuth 是个包装函数,帮你处理了重定向逻辑。你只需要在 authorized 回调里返回 truefalse,它会自动重定向到登录页。

我个人更喜欢 withAuth,代码更简洁。

那 getServerSession 在哪用?

getServerSession 是在 Server Component、API Route 和 Server Action 里用的。

在 Server Component 中:

// app/admin/page.tsx
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { redirect } from "next/navigation"

export default async function AdminPage() {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    redirect('/login')
  }
  
  if (session.user.role !== 'admin') {
    redirect('/403')
  }
  
  // 渲染页面
  return <UsersList />
}

这一层检查用户是否有权限访问这个页面。没权限就不让看。

在 API Route 中:

// app/api/users/route.ts
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { NextResponse } from "next/server"

export async function DELETE(req: Request) {
  const session = await getServerSession(authOptions)
  
  if (!session || session.user.role !== 'admin') {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
  }
  
  // 执行删除操作
  // ...
  
  return NextResponse.json({ success: true })
}

在 Server Action 中:

// app/actions.ts
'use server'

import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"

export async function deleteUser(userId: string) {
  const session = await getServerSession(authOptions)
  
  if (!session || session.user.role !== 'admin') {
    throw new Error('Unauthorized')
  }
  
  // 执行删除
  // ...
}

两者的分工

说白了就是:

  • Middleware(用 getToken):粗粒度的路由拦截,比如未登录统一重定向、基本的角色检查
  • getServerSession:细粒度的权限控制,在真正操作数据前做最后一道验证

打个比方,Middleware 是小区门口的保安,拦住明显不该进来的人。getServerSession 是你家门锁,就算保安放行了,没钥匙还是进不来。

多层防护,就是这个意思。

管理后台的 RBAC 完整设计

前面聊了 Middleware 和 getServerSession 的配合,但这些还比较零散。真正做一个管理后台,你需要一套完整的 RBAC(基于角色的访问控制)体系。

先理清楚 RBAC 的模型

RBAC 的核心思路是:用户 → 角色 → 权限。

  • 用户(User):张三、李四
  • 角色(Role):管理员、编辑、查看者
  • 权限(Permission):查看用户列表、编辑文章、删除评论

一个用户可以有多个角色,一个角色包含多个权限。比如张三是管理员,拥有所有权限;李四是编辑,只能编辑文章和查看用户列表。

权限的粒度可以分三种:

  • 页面级:能不能访问某个页面(如 /admin/users
  • 功能级:能不能点某个按钮(如”删除”按钮)
  • 数据级:能不能看某条数据(如只能看自己创建的文章)

数据库设计大概长这样(Prisma Schema):

model User {
  id    String @id @default(cuid())
  email String @unique
  roles Role[]
}

model Role {
  id          String       @id @default(cuid())
  name        String       @unique
  permissions Permission[]
  users       User[]
}

model Permission {
  id    String @id @default(cuid())
  name  String @unique // 如 "user:view", "user:edit"
  roles Role[]
}

实际项目中,你可能不需要这么复杂的数据库设计。如果角色不多(比如就管理员、编辑、查看者三种),直接在代码里定义就行了。

四层防护架构

接下来讲讲怎么把权限检查落实到每一层。

第一层:Middleware - 粗粒度拦截

// middleware.ts
import { withAuth } from "next-auth/middleware"

export default withAuth({
  callbacks: {
    authorized: ({ token, req }) => {
      if (!token) return false
      
      const path = req.nextUrl.pathname
      
      // admin 路径只允许 admin 角色
      if (path.startsWith('/admin')) {
        return token.role === 'admin'
      }
      
      // dashboard 路径要求登录即可
      if (path.startsWith('/dashboard')) {
        return true
      }
      
      return false
    }
  }
})

export const config = {
  matcher: ['/admin/:path*', '/dashboard/:path*']
}

这一层只做基本的角色判断,不涉及具体的功能权限。

第二层:Server Component - 页面级权限检查

// app/admin/users/page.tsx
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { checkPermission } from "@/lib/permissions"
import { redirect } from "next/navigation"

export default async function UsersPage() {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    redirect('/login')
  }
  
  // 检查是否有查看用户列表的权限
  const hasPermission = await checkPermission(session.user.id, 'user:view')
  
  if (!hasPermission) {
    redirect('/403')
  }
  
  // 渲染页面
  return <UsersList />
}

这一层检查用户是否有权限访问这个页面。没权限就不让看。

第三层:UI 条件渲染 - 功能级权限

// components/UsersList.tsx
'use client'

import { useSession } from "next-auth/react"
import { hasPermission } from "@/lib/permissions-client"

export function UsersList() {
  const { data: session } = useSession()
  
  const canEdit = hasPermission(session, 'user:edit')
  const canDelete = hasPermission(session, 'user:delete')
  
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>
          <span>{user.name}</span>
          {canEdit && <button>编辑</button>}
          {canDelete && <button>删除</button>}
        </div>
      ))}
    </div>
  )
}

这一层根据权限隐藏或显示按钮。用户看不到的按钮,自然也就不会点。

但注意,这只是 UX 优化,不是安全措施。懂点技术的人可以在浏览器控制台把按钮显示出来。真正的安全检查在下一层。

第四层:Server Action / API - 数据操作前验证

// app/actions.ts
'use server'

import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { checkPermission } from "@/lib/permissions"

export async function deleteUser(userId: string) {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    throw new Error('Unauthorized')
  }
  
  // 必须有删除权限
  const hasPermission = await checkPermission(session.user.id, 'user:delete')
  
  if (!hasPermission) {
    throw new Error('Forbidden')
  }
  
  // 执行删除
  await db.user.delete({ where: { id: userId } })
  
  return { success: true }
}

这是最后一道防线。无论前面的层有没有检查,到了数据操作这一步,必须再验证一次权限。

集中管理权限配置

每个地方都写一遍权限检查太麻烦了。我的做法是,把权限定义和检查逻辑都集中在一个文件里。

// lib/permissions.ts
export const PERMISSIONS = {
  USER_VIEW: 'user:view',
  USER_EDIT: 'user:edit',
  USER_DELETE: 'user:delete',
  POST_VIEW: 'post:view',
  POST_EDIT: 'post:edit',
  POST_DELETE: 'post:delete',
} as const

export const ROLES = {
  ADMIN: {
    name: 'admin',
    permissions: Object.values(PERMISSIONS) // 管理员有所有权限
  },
  EDITOR: {
    name: 'editor',
    permissions: [
      PERMISSIONS.USER_VIEW,
      PERMISSIONS.POST_VIEW,
      PERMISSIONS.POST_EDIT,
    ]
  },
  VIEWER: {
    name: 'viewer',
    permissions: [
      PERMISSIONS.USER_VIEW,
      PERMISSIONS.POST_VIEW,
    ]
  }
} as const

// 服务端权限检查
export async function checkPermission(userId: string, permission: string) {
  const user = await db.user.findUnique({
    where: { id: userId },
    include: { roles: true }
  })
  
  if (!user) return false
  
  // 检查用户的角色是否包含所需权限
  const userRole = ROLES[user.role as keyof typeof ROLES]
  return userRole?.permissions.includes(permission) ?? false
}
// lib/permissions-client.ts (客户端版本)
import { Session } from "next-auth"

export function hasPermission(session: Session | null, permission: string) {
  if (!session?.user) return false
  
  const userRole = ROLES[session.user.role as keyof typeof ROLES]
  return userRole?.permissions.includes(permission) ?? false
}

这样一来,权限的增删改都在一个文件里搞定,不用到处翻代码。

这套架构的好处

多写了好几层代码,值得吗?

值得。

  • 安全性:一层被绕过,还有其他层兜底
  • 可维护性:权限配置集中管理,改起来方便
  • 用户体验:该隐藏的按钮隐藏,不会让用户点了之后才告诉他没权限
  • 审计友好:每一层都有明确的权限检查,安全审查容易通过

说实话,一开始搭这套架构确实费点时间。但后面加新功能、改权限规则的时候,你会发现省了太多事。

实战案例与代码模板

前面讲了原理和架构,现在给你一些可以直接拿去用的代码模板和常见问题的排查方法。

完整的 Middleware 配置模板

这是一个功能完整的 Middleware 配置,支持多角色、动态路由匹配:

// middleware.ts
import { withAuth } from "next-auth/middleware"
import { NextResponse } from "next/server"

export default withAuth(
  function middleware(req) {
    const token = req.nextauth.token
    const path = req.nextUrl.pathname
    
    // 根据路径和角色进行细粒度控制
    if (path.startsWith('/admin') && token?.role !== 'admin') {
      return NextResponse.redirect(new URL('/403', req.url))
    }
    
    if (path.startsWith('/editor') && !['admin', 'editor'].includes(token?.role as string)) {
      return NextResponse.redirect(new URL('/403', req.url))
    }
    
    return NextResponse.next()
  },
  {
    callbacks: {
      authorized: ({ token }) => !!token
    }
  }
)

export const config = {
  matcher: [
    '/admin/:path*',
    '/editor/:path*',
    '/dashboard/:path*',
    '/api/admin/:path*',
    '/api/editor/:path*'
  ]
}

关键点

  1. matcher 定义哪些路径需要保护,支持通配符 :path*
  2. authorized 回调做最基本的登录检查
  3. middleware 函数里做更细的角色判断

动态菜单渲染

根据用户权限动态生成菜单,是管理后台的常见需求。这是一个简单实用的实现:

// components/Sidebar.tsx
'use client'

import { useSession } from "next-auth/react"
import Link from "next/link"
import { hasPermission } from "@/lib/permissions-client"

const menuItems = [
  {
    label: '用户管理',
    href: '/admin/users',
    permission: 'user:view'
  },
  {
    label: '文章管理',
    href: '/admin/posts',
    permission: 'post:view'
  },
  {
    label: '系统设置',
    href: '/admin/settings',
    permission: 'setting:manage'
  }
]

export function Sidebar() {
  const { data: session } = useSession()
  
  // 根据权限过滤菜单项
  const visibleItems = menuItems.filter(item => 
    hasPermission(session, item.permission)
  )
  
  return (
    <nav>
      {visibleItems.map(item => (
        <Link key={item.href} href={item.href}>
          {item.label}
        </Link>
      ))}
    </nav>
  )
}

把菜单配置和权限关联起来,一目了然。新增菜单项时,只要加到 menuItems 数组里就行。

可复用的权限检查 Hook

封装一个 React Hook,在客户端组件里更方便地使用:

// hooks/usePermission.ts
import { useSession } from "next-auth/react"
import { hasPermission } from "@/lib/permissions-client"

export function usePermission(permission: string) {
  const { data: session, status } = useSession()
  
  const isLoading = status === 'loading'
  const isAllowed = hasPermission(session, permission)
  
  return { isAllowed, isLoading }
}

使用起来很简洁:

// components/DeleteButton.tsx
'use client'

import { usePermission } from "@/hooks/usePermission"

export function DeleteButton({ userId }: { userId: string }) {
  const { isAllowed, isLoading } = usePermission('user:delete')
  
  if (isLoading) return <div>Loading...</div>
  if (!isAllowed) return null
  
  return (
    <button onClick={() => deleteUser(userId)}>
      删除
    </button>
  )
}

常见问题排查

问题1:Middleware 无限重定向

症状:访问页面时浏览器报”重定向次数过多”。

原因:登录页也被 Middleware 拦截了,导致循环重定向。

解决:在 matcher 中排除登录页和公开页面。

export const config = {
  matcher: [
    /*
     * 匹配所有路径,除了:
     * - /login (登录页)
     * - /api/auth (NextAuth API)
     * - /_next (Next.js 内部)
     * - /favicon.ico, /robots.txt (静态文件)
     */
    '/((?!login|api/auth|_next|favicon.ico|robots.txt).*)',
  ]
}

问题2:getServerSession 返回 null

症状:明明已经登录了,但 getServerSession 一直返回 null

原因:

  1. authOptions 配置不对,或者没传
  2. Cookie 设置问题(比如跨域、HTTPS)
  3. 在 Middleware 里错误地使用了 getServerSession

解决:

  • 检查 authOptions 是否正确导入
  • 确保在 Server Component 或 API Route 里使用,不要在 Middleware 里用
  • 开发环境检查 NEXTAUTH_URL 环境变量是否正确
// 正确的用法
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/app/api/auth/[...nextauth]/route"

const session = await getServerSession(authOptions)

问题3:权限检查性能问题

症状:每次页面加载都很慢,因为要查数据库验证权限。

原因:权限检查没做缓存,每个请求都查库。

解决:

  1. 把用户角色和权限放到 JWT token 里,避免查库
// app/api/auth/[...nextauth]/route.ts
export const authOptions: NextAuthOptions = {
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role
        token.permissions = user.permissions
      }
      return token
    },
    async session({ session, token }) {
      session.user.role = token.role
      session.user.permissions = token.permissions
      return session
    }
  }
}
  1. 使用 React Query 或 SWR 在客户端缓存权限查询结果
// hooks/usePermissions.ts
import useSWR from 'swr'

export function usePermissions() {
  const { data: permissions } = useSWR('/api/me/permissions', {
    revalidateOnFocus: false,
    dedupingInterval: 60000 // 1分钟内不重复请求
  })
  
  return permissions
}

快速检查清单

在上线前,用这个清单检查一下你的权限控制:

  • Middleware 是否只做粗粒度检查?
  • 所有 Server Component 页面是否都有权限验证?
  • 所有 API Route 是否都有权限验证?
  • 所有 Server Action 是否都有权限验证?
  • 敏感按钮是否根据权限隐藏?
  • 权限配置是否集中管理?
  • JWT token 里是否包含了角色信息?
  • 登录页和公开页面是否排除在 Middleware 之外?

全部打勾了,你的权限控制基本就稳了。

结论

回到最开始的那个问题:Next.js 的权限控制,到底该怎么做?

答案是:别指望一招鲜。

Middleware 很重要,它是第一道防线,能拦住大部分未授权的访问。但它不是全部。你还需要在 Server Component 里检查页面权限,在 Server Action 和 API Route 里验证操作权限,在 UI 层做好体验优化。

这套多层防护架构,乍一看挺麻烦的。但你想想,安全这事儿本来就没有银弹。一层被绕过了,还有其他层兜底,这才是靠谱的做法。

只用 Middleware vs 多层防护

对比维度只用 Middleware多层防护
安全性低,可被绕过高,多层兜底
开发成本低,一处配置中,需要在多处添加检查
维护性差,权限逻辑分散好,集中管理配置
用户体验一般,点了才知道没权限好,提前隐藏不可用功能
审计友好差,检查点单一好,每层都有记录

看到了吧,多层防护虽然麻烦点,但好处是实实在在的。

立即行动

如果你正在做 Next.js 项目,不妨现在就检查一下:

  1. 你的项目是不是只在 Middleware 里做了权限控制?
  2. API Route 和 Server Action 有没有独立的权限验证?
  3. 权限配置是不是散落在各个文件里?

如果有任何一条答案是”是”,建议尽快重构成多层防护架构。不用一步到位,可以先把最关键的数据操作层加上权限检查,再慢慢完善其他层。

安全问题,早修比晚修强。

还有问题?

这篇文章聊了 Next.js 权限控制的核心架构和实战方案,但肯定没法覆盖所有场景。如果你在实际项目中遇到了其他问题,比如:

  • 数据级权限怎么做(只能看自己创建的数据)
  • 如何结合 Prisma 的 Row-Level Security
  • 多租户系统的权限隔离

可以在评论区留言,咱们一起探讨。

权限控制这个话题,永远不会过时。希望这篇文章能帮你少踩几个坑。

Next.js多层权限防护完整配置流程

从Middleware基础检查到getServerSession详细验证、数据库权限检查的完整步骤

⏱️ 预计耗时: 3 小时

  1. 1

    步骤1: 第一层:Middleware基础检查

    创建middleware.ts:
    ```ts
    import { withAuth } from 'next-auth/middleware'

    export default withAuth({
    pages: {
    signIn: '/login'
    }
    })

    export const config = {
    matcher: ['/dashboard/:path*', '/admin/:path*']
    }
    ```

    作用:
    • 检查用户是否登录
    • 未登录重定向到登录页
    • 速度快、位置早

    限制:
    • 可以绕过(x-middleware-subrequest漏洞)
    • 只能做粗筛
    • 不能做详细权限检查

    关键点:Middleware是第一层防护,但不是唯一防护。
  2. 2

    步骤2: 第二层:getServerSession详细验证

    在页面中使用:
    ```tsx
    import { getServerSession } from 'next-auth'
    import { authOptions } from '@/app/api/auth/[...nextauth]/route'

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

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

    // 检查用户角色
    if (session.user.role !== 'admin') {
    redirect('/unauthorized')
    }

    return <div>Dashboard</div>
    }
    ```

    作用:
    • 详细验证用户身份
    • 检查用户角色
    • 细粒度权限控制

    优势:
    • 比Middleware更安全
    • 可以做详细权限检查
    • 无法绕过

    关键点:getServerSession是第二层防护,做详细验证。
  3. 3

    步骤3: 第三层:数据库权限检查

    在API Route中检查:
    ```ts
    import { getServerSession } from 'next-auth'
    import { db } from '@/lib/db'

    export async function GET(request: Request) {
    const session = await getServerSession(authOptions)

    if (!session) {
    return new Response('Unauthorized', { status: 401 })
    }

    // 数据库权限检查
    const user = await db.user.findUnique({
    where: { id: session.user.id },
    include: { role: { include: { permissions: true } } }
    })

    const hasPermission = user.role.permissions.some(
    p => p.resource === 'users' && p.action === 'read'
    )

    if (!hasPermission) {
    return new Response('Forbidden', { status: 403 })
    }

    // 返回数据
    return Response.json({ users: [...] })
    }
    ```

    作用:
    • 最终权限验证
    • 检查数据库中的权限
    • 确保数据安全

    关键点:数据库是第三层防护,最终验证。
  4. 4

    步骤4: 实现RBAC权限体系

    数据库Schema:
    ```prisma
    model User {
    id String @id @default(cuid())
    email String @unique
    role Role @relation(fields: [roleId], references: [id])
    roleId String
    }

    model Role {
    id String @id @default(cuid())
    name String @unique
    permissions Permission[]
    users User[]
    }

    model Permission {
    id String @id @default(cuid())
    resource String
    action String
    roles Role[]
    }
    ```

    检查权限:
    ```ts
    async function checkPermission(
    userId: string,
    resource: string,
    action: string
    ) {
    const user = await db.user.findUnique({
    where: { id: userId },
    include: { role: { include: { permissions: true } } }
    })

    return user.role.permissions.some(
    p => p.resource === resource && p.action === action
    )
    }
    ```

    关键点:
    • 用户有角色
    • 角色有权限
    • 权限控制资源访问

常见问题

为什么权限控制不能只靠Middleware?
原因:Middleware可以绕过。

漏洞案例:
• CVE-2025-29927:攻击者可以通过在请求头里加x-middleware-subrequest字段,直接绕过Middleware的检查
• 用Postman直接调后台API,轻松拿到被保护的数据

Middleware的定位:
• 运行在Edge Runtime,做粗筛
• 检查用户是否登录、是管理员还是普通用户
• 速度快、位置早,但只靠它不够

解决方案:多层防护
• 第一层:Middleware(基础检查)
• 第二层:getServerSession(详细验证)
• 第三层:数据库权限检查(最终验证)

关键点:三层防护确保安全,任何一层都不能省略。
Middleware和getServerSession有什么区别?
Middleware:
• 运行在Edge Runtime
• 速度快、位置早
• 只能做粗筛(检查登录状态、用户角色)
• 可以绕过(x-middleware-subrequest漏洞)

getServerSession:
• 运行在Node.js Runtime
• 可以做详细验证
• 检查用户角色、权限
• 无法绕过

配合使用:
• Middleware做基础检查(第一层)
• getServerSession做详细验证(第二层)
• 数据库做最终权限检查(第三层)

代码示例:
```ts
// middleware.ts(第一层)
export default withAuth({
pages: { signIn: '/login' }
})

// page.tsx(第二层)
const session = await getServerSession(authOptions)
if (session.user.role !== 'admin') {
redirect('/unauthorized')
}

// api/route.ts(第三层)
const hasPermission = await checkPermission(userId, 'users', 'read')
if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}
```
如何实现RBAC权限体系?
RBAC = Role-Based Access Control(基于角色的访问控制)

数据库Schema:
```prisma
model User {
id String @id
email String @unique
role Role @relation(fields: [roleId], references: [id])
roleId String
}

model Role {
id String @id
name String @unique
permissions Permission[]
users User[]
}

model Permission {
id String @id
resource String // 资源:users, posts, etc.
action String // 操作:read, write, delete
roles Role[]
}
```

检查权限:
```ts
async function checkPermission(
userId: string,
resource: string,
action: string
) {
const user = await db.user.findUnique({
where: { id: userId },
include: { role: { include: { permissions: true } } }
})

return user.role.permissions.some(
p => p.resource === resource && p.action === action
)
}
```

关键点:
• 用户有角色
• 角色有权限
• 权限控制资源访问
如何实现多层防护架构?
三层防护:

第一层:Middleware(基础检查)
```ts
// middleware.ts
export default withAuth({
pages: { signIn: '/login' }
})
```

第二层:getServerSession(详细验证)
```tsx
// page.tsx
const session = await getServerSession(authOptions)
if (session.user.role !== 'admin') {
redirect('/unauthorized')
}
```

第三层:数据库权限检查(最终验证)
```ts
// api/route.ts
const hasPermission = await checkPermission(userId, 'users', 'read')
if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}
```

关键点:
• 三层防护确保安全
• 任何一层都不能省略
• Middleware做粗筛,getServerSession做细筛,数据库做最终验证
如何防止x-middleware-subrequest漏洞?
漏洞:攻击者可以通过在请求头里加x-middleware-subrequest字段,直接绕过Middleware的检查。

防护方法:
• 不要只靠Middleware
• 在API Route中也要检查权限
• 使用getServerSession验证
• 数据库做最终权限检查

代码示例:
```ts
// api/route.ts
export async function GET(request: Request) {
// 即使Middleware被绕过,这里也会检查
const session = await getServerSession(authOptions)

if (!session) {
return new Response('Unauthorized', { status: 401 })
}

// 数据库权限检查
const hasPermission = await checkPermission(session.user.id, 'users', 'read')

if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}

return Response.json({ users: [...] })
}
```

关键点:
• 多层防护
• API Route也要检查权限
• 数据库做最终验证

建议:永远不要只靠Middleware,要在API Route中也检查权限。
权限控制的最佳实践是什么?
三层防护:
• Middleware做基础检查(第一层)
• getServerSession做详细验证(第二层)
• 数据库做最终权限检查(第三层)

RBAC权限体系:
• 用户有角色
• 角色有权限
• 权限控制资源访问

代码组织:
• 创建权限检查工具函数
• 在API Route中统一使用
• 避免重复代码

安全建议:
• 永远不要只靠Middleware
• 在API Route中也要检查权限
• 数据库做最终验证
• 定期审查权限配置

关键点:
• 多层防护确保安全
• 任何一层都不能省略
• 持续改进权限体系

记住:权限控制是安全的基础,不能掉以轻心。

15 分钟阅读 · 发布于: 2025年12月19日 · 修改于: 2026年1月22日

评论

使用 GitHub 账号登录后即可评论

相关文章