6月4日 15:42

NestJS中间件和守卫有什么区别?各自适用场景和RBAC实现

NestJS 里中间件和守卫都能拦截请求,很多人搞不清该用哪个。一句话区分:中间件不知道下一站是谁,守卫知道。中间件只能看到原始的 HTTP 请求/响应,守卫能拿到 ExecutionContext,知道当前请求要调用哪个控制器、哪个方法。这个区别决定了各自的职责:中间件做通用预处理(日志、CORS),守卫做权限判断(认证、授权)。

核心区别对比

中间件(Middleware)守卫(Guard)
能看到什么reqresnextExecutionContext(含控制器、方法元信息)
能否访问 DI 容器不能(函数式中间件)可以(@Injectable()
作用范围模块级或全局方法级、控制器级、全局
能否用装饰器元数据不能能(Reflector + SetMetadata
执行时机最早(路由匹配之前)守卫之后,管道之前
典型用途日志、CORS、请求转换认证、授权、角色检查
返回值无(调 next() 放行)boolean / Promise<boolean>

中间件:看不到终点站的通用处理

中间件直接来自 Express 的概念,签名是 (req, res, next) => void

typescript
import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; @Injectable() export class LoggerMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { console.log(`${req.method} ${req.url}`); next(); // 放行,必须调,否则请求卡住 } }

在模块里注册(中间件不能装饰器注册):

typescript
@Module({}) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply(LoggerMiddleware) .forRoutes('users') // 只对 /users 路由生效 .exclude({ path: 'users', method: RequestMethod.GET }) // 排除 GET } }

中间件能做什么

  • 日志:记录请求方法、路径、耗时
  • CORS:跨域配置
  • 请求转换:解析 body、压缩响应
  • 限流:简单的 IP 级别限流

中间件不能做什么

  • 权限判断:中间件拿不到当前要调用的控制器方法,不知道这个接口需要什么角色
  • 读取装饰器元数据Reflector 在中间件里不可用
  • 精细路由控制:只能在模块级别通过路径匹配,不能按方法粒度

守卫:知道要去哪,所以能判断能不能去

守卫实现 CanActivate 接口,返回 true 放行、false 拒绝(返回 403):

typescript
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; @Injectable() export class AuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); return !!request.user; // 有 user 就放行 } }

使用:

typescript
@Controller('users') @UseGuards(AuthGuard) // 整个控制器都要认证 export class UsersController { @Get() findAll() { /* ... */ } @Post() @UseGuards(AdminGuard) // 这个方法额外要管理员权限 create() { /* ... */ } }

基于角色的权限控制(RBAC)

守卫真正的威力是配合 SetMetadata + Reflector 实现声明式权限:

typescript
// 自定义装饰器 import { SetMetadata } from '@nestjs/common'; export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
typescript
// 守卫里读取元数据 import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { // 拿到方法或控制器上 @Roles() 标注的角色 const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [ context.getHandler(), // 方法级别的元数据 context.getClass(), // 控制器级别的元数据 ]); if (!requiredRoles) return true; // 没标注角色,放行 const request = context.switchToHttp().getRequest(); return requiredRoles.some(role => request.user?.roles?.includes(role)); } }

控制器上使用:

typescript
@Controller('admin') @UseGuards(AuthGuard, RolesGuard) @Roles('admin') // 整个控制器需要 admin 角色 export class AdminController { @Get('dashboard') dashboard() { /* ... */ } @Get('users') @Roles('admin', 'superadmin') // 这个方法需要 admin 或 superadmin listUsers() { /* ... */ } }

这是中间件做不到的——中间件拿不到 @Roles('admin') 这个元数据,也不知道当前请求匹配的是哪个方法。

守卫里注入服务

守卫是 @Injectable() 的,可以注入数据库、缓存等服务:

typescript
@Injectable() export class AuthGuard implements CanActivate { constructor(private jwtService: JwtService) {} async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) return false; try { request.user = await this.jwtService.verifyAsync(token); return true; } catch { return false; } } }

执行顺序

一个请求经过的完整链路:

shell
客户端请求 → 中间件 → 守卫 → 拦截器(before) → 管道 → 控制器 → 拦截器(after) → 过滤器

中间件最先执行,适合做不依赖业务逻辑的通用处理。守卫在中间件之后,能用中间件预处理的结果(如解析出的 token)做权限判断。权限不通过直接返回 403,不会走到管道和控制器。

什么时候用哪个

场景用什么原因
请求日志中间件不需要知道目标方法
CORS 配置中间件通用 HTTP 头处理
请求限流中间件按 IP/路由限流,不涉及业务
JWT 验证守卫需要注入 JwtService,需要设置 request.user
角色权限守卫需要读取 @Roles() 元数据
API Key 验证守卫需要查询数据库验证 key
请求体转换中间件纯数据处理,不涉及权限
多租户隔离守卫需要根据路由决定查询哪个租户的数据

判断口诀:只看 HTTP 不看业务 → 中间件;要看路由决定权限 → 守卫

标签:NestJS