为什么需要中间件权限控制?
在 Next.js 应用中,权限控制通常有两种方式:在页面组件中检查用户状态(客户端渲染时),或者在 API 路由中验证。但前者会导致页面闪动(先渲染再跳转),后者无法保护页面路由。Next.js 中间件(Middleware)运行在请求到达页面之前,是进行路由级权限控制的理想位置。它允许你在服务端(Edge Runtime)执行代码,根据 Cookie、Header 等信息决定是否放行请求,从而实现无缝的权限拦截。
本文将从零开始,构建一个基于 JWT 的权限控制系统,涵盖:
- 中间件基础配置与匹配规则
- 用户认证状态检查
- 基于角色的访问控制(RBAC)
- 细粒度权限校验(例如 admin 才能访问 /admin)
- 常见坑与最佳实践
环境准备
假设你已经有一个 Next.js 项目(版本 13+,使用 App Router)。如果没有,可以快速创建:
npx create-next-app@latest my-auth-app --typescript --app
cd my-auth-app
安装必要的依赖:
npm install jose jsonwebtoken
我们将使用 jose 在 Edge Runtime 中处理 JWT(因为 jsonwebtoken 依赖 Node.js 的 crypto,不能在 Edge 运行)。
第一步:设计认证流程
为了演示,我们简化认证流程:
- 用户登录后,服务端生成 JWT,包含用户 ID 和角色(role),并设置到 HttpOnly Cookie 中。
- 中间件读取 Cookie,验证 JWT,根据角色决定是否允许访问。
- 未登录用户访问受保护路由时,重定向到登录页。
我们还需要一个登录 API 路由来生成 JWT。先创建 app/api/login/route.ts:
import { NextResponse } from 'next/server';
import { SignJWT } from 'jose';
const secret = new TextEncoder().encode(process.env.JWT_SECRET || 'your-secret-key');
export async function POST(request: Request) {
const { username, password } = await request.json();
// 模拟验证,实际应查询数据库
if (username !== 'admin' || password !== 'admin123') {
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
}
const token = await new SignJWT({ sub: '1', role: 'admin' })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('1h')
.sign(secret);
const response = NextResponse.json({ success: true });
response.cookies.set('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 3600,
path: '/',
});
return response;
}
注意:实际项目中密码应哈希存储,并使用环境变量管理密钥。
第二步:编写中间件
在项目根目录创建 middleware.ts(与 app 同级)。中间件函数接收 NextRequest,返回 NextResponse。
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';
const secret = new TextEncoder().encode(process.env.JWT_SECRET || 'your-secret-key');
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
// 公开路径,直接放行
const publicPaths = ['/login', '/api/login'];
if (publicPaths.some(path => request.nextUrl.pathname.startsWith(path))) {
return NextResponse.next();
}
// 未登录用户重定向到登录页
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
try {
const { payload } = await jwtVerify(token, secret);
const role = payload.role as string;
// 基于角色的路由控制
if (request.nextUrl.pathname.startsWith('/admin') && role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
// 将用户信息添加到请求头,供后续使用
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', payload.sub as string);
requestHeaders.set('x-user-role', role);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
} catch (error) {
// token 无效或过期
return NextResponse.redirect(new URL('/login', request.url));
}
}
// 配置匹配器,排除静态资源和 API 路由(根据需要调整)
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
这个中间件做了以下几件事:
- 放行公开路径(登录页和登录 API)。
- 检查 token 是否存在,不存在则跳转登录页。
- 验证 JWT,检查角色,对
/admin路径进行角色限制。 - 将用户信息注入请求头,方便后续服务端组件或 API 路由使用。
第三步:在服务端组件中获取用户信息
中间件修改后的请求头可以在服务端组件中通过 headers() 读取:
import { headers } from 'next/headers';
export default function AdminPage() {
const headersList = headers();
const role = headersList.get('x-user-role');
// 中间件已确保只有 admin 能到达这里,但可以再次验证
return <div>Welcome, {role}!</div>;
}
第四步:细粒度权限控制
除了路由级别的控制,有时需要更细粒度的权限,例如某个页面内不同操作需要不同权限。可以在中间件中根据路径和方法进行判断。
假设我们有一个 /api/posts 路由,GET 允许所有登录用户,POST 仅允许 admin。可以在中间件中处理:
if (request.nextUrl.pathname.startsWith('/api/posts')) {
const method = request.method;
if (method === 'POST' && role !== 'admin') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
}
但更推荐的做法是在 API 路由内部使用一个权限检查工具函数,因为中间件中做太多业务逻辑会变得臃肿。中间件适合做粗粒度的路由守卫,细粒度逻辑应放在路由处理器中。
常见坑与最佳实践
1. 中间件运行在 Edge Runtime,不能使用 Node.js API
不要尝试在中间件中引入 fs、crypto 等 Node.js 模块。使用 jose 替代 jsonwebtoken,使用 Web Crypto API 替代 Node crypto。
2. 避免在中间件中执行数据库查询
中间件在每个请求上运行,包括静态资源。数据库查询会显著增加延迟。尽量只使用 JWT 中携带的信息(如角色),或者将权限数据缓存到 Cookie 中。
3. 正确配置 matcher
默认情况下,中间件会匹配所有路由,包括 _next/static 等。务必使用 matcher 排除静态资源,否则可能影响性能。
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
但注意,如果需要在中间件中处理 API 路由,则不要排除 api。
4. 处理 CORS 和预检请求
如果中间件拦截了 API 路由,需要正确处理 OPTIONS 请求,否则跨域请求会失败。可以在中间件中放行 OPTIONS 方法。
5. 安全地设置 Cookie
使用 httpOnly、secure、sameSite 属性防止 XSS 和 CSRF。生产环境务必启用 secure。
6. 避免重定向循环
确保登录页和公开路径不会被中间件再次拦截。检查 matcher 和公开路径列表。
总结
通过 Next.js 中间件,我们可以在请求到达页面前进行权限校验,实现无缝的路由守卫。本文介绍了从 JWT 验证到角色控制的完整流程,并提供了可运行的代码。关键点:
- 中间件适合做粗粒度路由控制,细粒度权限应放在业务逻辑层。
- 使用
jose在 Edge Runtime 中处理 JWT。 - 通过请求头传递用户信息,方便服务端组件使用。
- 注意 matcher 配置和安全性设置。
下一步,你可以扩展此方案,例如集成第三方认证(NextAuth.js)、实现基于权限的 UI 渲染,或者使用中间件进行 A/B 测试。希望本文能帮助你构建更安全的 Next.js 应用。