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 回调里返回 true 或 false,它会自动重定向到登录页。
我个人更喜欢 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*'
]
}关键点:
matcher定义哪些路径需要保护,支持通配符:path*authorized回调做最基本的登录检查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。
原因:
authOptions配置不对,或者没传- Cookie 设置问题(比如跨域、HTTPS)
- 在 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:权限检查性能问题
症状:每次页面加载都很慢,因为要查数据库验证权限。
原因:权限检查没做缓存,每个请求都查库。
解决:
- 把用户角色和权限放到 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
}
}
}- 使用 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 项目,不妨现在就检查一下:
- 你的项目是不是只在 Middleware 里做了权限控制?
- API Route 和 Server Action 有没有独立的权限验证?
- 权限配置是不是散落在各个文件里?
如果有任何一条答案是”是”,建议尽快重构成多层防护架构。不用一步到位,可以先把最关键的数据操作层加上权限检查,再慢慢完善其他层。
安全问题,早修比晚修强。
还有问题?
这篇文章聊了 Next.js 权限控制的核心架构和实战方案,但肯定没法覆盖所有场景。如果你在实际项目中遇到了其他问题,比如:
- 数据级权限怎么做(只能看自己创建的数据)
- 如何结合 Prisma 的 Row-Level Security
- 多租户系统的权限隔离
可以在评论区留言,咱们一起探讨。
权限控制这个话题,永远不会过时。希望这篇文章能帮你少踩几个坑。
Next.js多层权限防护完整配置流程
从Middleware基础检查到getServerSession详细验证、数据库权限检查的完整步骤
⏱️ 预计耗时: 3 小时
- 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: 第二层: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: 第三层:数据库权限检查
在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: 实现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?
漏洞案例:
• CVE-2025-29927:攻击者可以通过在请求头里加x-middleware-subrequest字段,直接绕过Middleware的检查
• 用Postman直接调后台API,轻松拿到被保护的数据
Middleware的定位:
• 运行在Edge Runtime,做粗筛
• 检查用户是否登录、是管理员还是普通用户
• 速度快、位置早,但只靠它不够
解决方案:多层防护
• 第一层:Middleware(基础检查)
• 第二层:getServerSession(详细验证)
• 第三层:数据库权限检查(最终验证)
关键点:三层防护确保安全,任何一层都不能省略。
Middleware和getServerSession有什么区别?
• 运行在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权限体系?
数据库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漏洞?
防护方法:
• 不要只靠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日
相关文章
Next.js 电商实战:购物车与 Stripe 支付完整实现指南

Next.js 电商实战:购物车与 Stripe 支付完整实现指南
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战

Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 单元测试实战:Jest + React Testing Library 完整配置指南


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