Next.js 中间件实战:构建灵活的权限控制系统

By | 2026年8月9日

为什么需要中间件做权限控制?

在 Next.js 应用中,权限控制是常见的需求。虽然可以在页面组件内部通过 getServerSidePropsgetStaticProps 校验权限,但这样存在几个问题:

  • 代码分散:每个需要保护的页面都要重复写校验逻辑。
  • 性能开销:在服务端渲染前无法提前拦截,可能产生不必要的计算。
  • 无法保护静态资源:如 public 目录下的文件。

Next.js 中间件(Middleware)允许你在请求完成前执行代码,非常适合做统一的权限控制。它运行在 Edge Runtime,性能极高,且能灵活匹配路径,实现细粒度的权限管理。

中间件的工作原理

中间件是一个名为 middleware.ts(或 .js)的文件,位于项目根目录(与 pagesapp 同级)。它在每次请求时都会执行,可以修改请求或响应,例如重写、重定向、添加请求头等。


// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // 在这里处理请求
  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*'],
};
  • middleware 函数接收 NextRequest 对象,返回 NextResponse
  • config.matcher 定义匹配路径,支持通配符。

注意:中间件在 Edge Runtime 运行,因此不能使用 Node.js 原生 API(如 fs),但可以使用 jose 库进行 JWT 验证。

实战:构建权限控制系统

假设我们有一个应用,包含公开页面(如首页、登录页)和受保护页面(如仪表盘、设置页)。我们将实现以下功能:

  1. 未登录用户访问受保护页面时,重定向到登录页。
  2. 已登录用户访问登录页时,重定向到仪表盘。
  3. 不同角色(如 admin、user)访问不同权限的页面。

步骤 1:安装依赖

我们需要 jose 来验证 JWT,以及 cookies 辅助函数(可选)。


npm install jose

步骤 2:创建中间件

在项目根目录创建 middleware.ts


// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';

// 定义受保护路径及对应角色(简单示例)
const protectedPaths = [
  { path: '/dashboard', roles: ['admin', 'user'] },
  { path: '/settings', roles: ['admin'] },
];

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 检查是否为受保护路径
  const matched = protectedPaths.find(p => pathname.startsWith(p.path));
  if (!matched) {
    return NextResponse.next();
  }

  // 获取 token(从 cookie 中)
  const token = request.cookies.get('token')?.value;
  if (!token) {
    // 未登录,重定向到登录页,并带上原路径
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('redirect', pathname);
    return NextResponse.redirect(loginUrl);
  }

  try {
    // 验证 JWT
    const secret = new TextEncoder().encode(process.env.JWT_SECRET);
    const { payload } = await jwtVerify(token, secret);

    // 检查角色权限
    const userRole = payload.role as string;
    if (!matched.roles.includes(userRole)) {
      // 无权限,重定向到首页或 403 页
      return NextResponse.redirect(new URL('/', request.url));
    }

    // 有权限,继续请求
    return NextResponse.next();
  } catch (error) {
    // token 无效,清除 cookie 并重定向到登录页
    const response = NextResponse.redirect(new URL('/login', request.url));
    response.cookies.delete('token');
    return response;
  }
}

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

解释

  • protectedPaths 数组定义了路径前缀和允许的角色。
  • matcher 中,我们额外匹配了 /login,以便在已登录时重定向到仪表盘。
  • 使用 jwtVerify 验证 token,并读取 payload.role 进行角色判断。
  • 如果 token 无效,删除 cookie 并重定向到登录页。

步骤 3:登录逻辑(示例)

为了演示,我们在登录页面模拟登录,设置 cookie。


// app/login/page.tsx
import { cookies } from 'next/headers';
import { SignJWT } from 'jose';

export default function LoginPage() {
  async function handleLogin(formData: FormData) {
    'use server';
    const username = formData.get('username');
    const password = formData.get('password');

    // 模拟验证,实际应查数据库
    if (username === 'admin' && password === '123456') {
      const secret = new TextEncoder().encode(process.env.JWT_SECRET);
      const token = await new SignJWT({ role: 'admin', username })
        .setProtectedHeader({ alg: 'HS256' })
        .setExpirationTime('1h')
        .sign(secret);

      cookies().set('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production' });
      // 重定向到仪表盘
      redirect('/dashboard');
    } else {
      // 登录失败
    }
  }

  return (
    <form action={handleLogin}>
      <input name="username" placeholder="用户名" />
      <input name="password" type="password" placeholder="密码" />
      <button type="submit">登录</button>
    </form>
  );
}

步骤 4:保护 API 路由与 Server Actions

中间件同样可以保护 API 路由,但需要注意:API 路由通常返回 JSON 而不是重定向。我们可以在中间件中判断请求类型,如果是 API 请求,则返回 401 状态码。


// 在 middleware.ts 中增加判断
if (pathname.startsWith('/api/')) {
  // 验证 token,如果无效返回 401
  if (!token || !(await verifyToken(token))) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }
  return NextResponse.next();
}

对于 Server Actions,由于它们也是 POST 请求,同样可以通过中间件保护。但注意 Server Actions 的请求路径是 /_actions/...,我们可以匹配这些路径。

性能优化与最佳实践

1. 使用 matcher 精确匹配

避免匹配所有路径,仅匹配需要保护的路径,减少中间件执行次数。

2. 避免在中间件中进行复杂计算

中间件运行在 Edge,资源有限,应只做轻量级操作。例如,JWT 验证是必须的,但不要进行数据库查询。

3. 使用 jose 而不是 jsonwebtoken

jose 支持 Edge Runtime,而 jsonwebtoken 依赖 Node.js API,无法在中间件中使用。

4. 缓存验证结果?

由于中间件每次请求都会执行,如果 token 验证频繁,可以考虑将验证结果缓存到 request 对象上,但 Edge 环境不支持跨请求共享状态,因此一般不做。

5. 注意 Cookie 的 httpOnly 设置

为了安全,建议设置 httpOnly: true,防止 XSS 攻击。

常见坑与解决方案

坑 1:中间件不生效

  • 确保 middleware.ts 位于项目根目录(与 apppages 同级)。
  • 检查 matcher 是否正确,路径需要匹配。
  • 如果使用 Next.js 12 以下版本,需要升级或使用自定义 server。

坑 2:无法使用 Node.js API

中间件在 Edge Runtime 运行,不能使用 fspath 等模块。如果需要使用,可以改用 getServerSideProps 或 API 路由。

坑 3:JWT 验证失败

  • 确保 jose 版本正确,使用 import { jwtVerify } from 'jose'
  • 确保 JWT_SECRET 环境变量已设置,且与签发时一致。

坑 4:重定向循环

如果登录页也受保护,且已登录用户访问登录页时重定向到仪表盘,但仪表盘又重定向回登录页,就会造成循环。解决方法是:在登录页的中间件逻辑中,如果已登录则直接重定向到仪表盘,不再继续。

总结

通过 Next.js 中间件,我们可以集中实现权限控制,避免在每个页面重复编写逻辑。中间件运行在 Edge,性能高效,且支持灵活的路径匹配和重定向。本文演示了如何实现基于 JWT 和角色的权限控制,并提供了常见问题的解决方案。

延伸阅读

希望这篇文章能帮助你更好地利用 Next.js 中间件构建安全的应用。如果你有更多问题,欢迎在评论区讨论!