Next.js 鉴权、权限与 Proxy
这块很容易被混成一件事,但实际至少有三层:
Authentication:确认当前用户是谁Session Management:让登录状态能跨请求延续Authorization:决定当前用户能看什么、能做什么
如果只把它理解成“登录拦截”,后面很容易把所有逻辑都堆进 middleware.ts 或 proxy.ts,最后既难维护,也不够安全。
先说当前主线
按照较新的 Next.js 官方文档,可以把鉴权相关逻辑分成几层:
1. 登录和会话建立
通常发生在:
- 登录表单提交
- OAuth 回调
- Server Action
- Route Handler
2. 乐观路由拦截
通常发生在:
proxy.ts- 旧项目里的
middleware.ts
这层适合做快速判断,例如:
- 未登录就跳到
/login - 已登录访问公开登录页时跳回
/dashboard - 按 cookie 里的轻量 session 做初步分流
3. 真正的权限校验
应该尽量贴近数据访问层、Server Component、Route Handler 或 Server Action。
这才是防线核心。
为什么 Proxy 不是完整鉴权方案
Next.js 16 开始,官方把 Middleware 改名成了 Proxy,就是为了强调它更像“请求边界上的拦截层”,而不是万能中间件。
官方文档里有几个很重要的提醒:
Proxy适合做 optimistic checks- 不应该承担完整 session 管理和授权逻辑
- 不适合慢数据请求
- 在
Proxy里使用fetch的缓存配置没有效果
所以比较稳的理解是:
Proxy负责“先挡一下明显不该进来的请求”- 真正的数据访问权限,放在离数据更近的地方再检查一次
一个典型的分层方式
比较推荐的组织方式大概是这样:
app/
dashboard/
api/
lib/
auth/
session.ts
permissions.ts
dal.ts
proxy.ts
可以把职责拆成:
session.ts:创建、解密、刷新、删除 sessionpermissions.ts:角色和权限判断dal.ts:带授权检查的数据访问入口proxy.ts:只做路由级乐观拦截
这样一来,权限不会散落在每个页面里,也不会全部挤进 proxy.ts。
Session 常见有两种路线
1. Stateless Session
通常用加密 cookie 存 session 信息。
优点:
- 架构简单
- 适合中小型应用
- 不一定需要单独 session 表
代价:
- 更新和失效策略要设计清楚
- cookie 内容不能乱放敏感数据
2. Database Session
把 session 存数据库,cookie 里只放 session id。
优点:
- 更容易集中管理
- 更容易做吊销、强制下线、多端控制
代价:
- 每次校验路径更长
- 需要多一层存储和查询
proxy.ts 里适合做什么
比较适合:
- 判断公开路由和受保护路由
- 读取 cookie 做轻量 session 判断
- 未登录跳转登录页
- 已登录访问登录页时重定向
- 做很轻的实验分流或租户前缀分流
一个最小例子
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const protectedRoutes = ['/dashboard', '/settings'];
const publicRoutes = ['/login', '/signup'];
export async function proxy(req: NextRequest) {
const path = req.nextUrl.pathname;
const isProtectedRoute = protectedRoutes.some((route) =>
path.startsWith(route)
);
const isPublicRoute = publicRoutes.some((route) => path.startsWith(route));
const session = req.cookies.get('session')?.value;
if (isProtectedRoute && !session) {
return NextResponse.redirect(new URL('/login', req.nextUrl));
}
if (isPublicRoute && session) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl));
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
};
这个例子的重点不在“完整可用”,而在边界感:
- 这里只做快速判断
- 没有把复杂权限系统塞进去
- 没有 依赖慢查询
proxy.ts 里不适合做什么
1. 不适合做完整授权系统
例如:
- 根据数据库里复杂角色表做细粒度权限判断
- 每个请求都去查多张表再决定能不能进
- 在这一层承担最终数据安全责任
2. 不适合慢请求
Proxy 跑在请求前面,如果这里变慢,整站入口都会被拖慢。
3. 不适合当唯一安全防线
只要接口、Server Action、Server Component 还能直接访问数据,就必须在这些地方再校验一次。
真正安全的检查应该放在哪里
1. Data Access Layer
官方文档很强调 DAL,也就是数据访问层。
例如:
import { cache } from 'react';
import { verifySession } from './session';
export const getCurrentUser = cache(async () => {
const session = await verifySession();
if (!session?.userId) {
return null;
}
return db.user.findById(session.userId);
});
再比如:
export async function getProjectById(projectId: string) {
const user = await getCurrentUser();
if (!user) {
throw new Error('unauthorized');
}
const project = await db.project.findById(projectId);
if (project.ownerId !== user.id) {
throw new Error('forbidden');
}
return project;
}
这层才是真正决定“能不能拿到数据”的地方。
2. Server Components
页面本身如果要读敏感数据,也可以直接在服务端组件里做校验。
import { redirect } from 'next/navigation';
import { getCurrentUser } from '@/lib/auth/dal';
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user) {
redirect('/login');
}
return <div>Welcome {user.name}</div>;
}
这适合页面级保护。
3. Server Actions
写操作一定要再做一次鉴权和授权。
'use server';
import { getCurrentUser } from '@/lib/auth/dal';
export async function deletePost(postId: string) {
const user = await getCurrentUser();
if (!user) {
throw new Error('unauthorized');
}
const post = await db.post.findById(postId);
if (post.authorId !== user.id) {
throw new Error('forbidden');
}
await db.post.delete(postId);
}
这里不能因为页面前面已经有 proxy 就省略校验。
4. Route Handlers
如果接口会被客户端、第三方系统、小程序、移动端调用,那权限检查更应该留在 Route Handler 里。
import { NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth/dal';
export async function GET() {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
}
const invoices = await db.invoice.findManyByUser(user.id);
return NextResponse.json(invoices);
}