Skip to content

中间件与守卫

一、Middleware(中间件)

本质

NestJS Middleware 就是 Express 中间件——接收 (req, res, next) 的函数。NestJS 只是在其外面包了一层 @Injectable() 装饰器,让它能参与依赖注入。

基础定义

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) {
    const start = Date.now();
    const { method, originalUrl } = req;

    // 在响应结束时记录耗时
    res.on('finish', () => {
      const elapsed = Date.now() - start;
      console.log(`${method} ${originalUrl} ${res.statusCode} - ${elapsed}ms`);
    });

    next(); // 必须调用!否则请求永远卡住
  }
}

注册方式

typescript
@Module({})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      // 精确指定路径
      .forRoutes({ path: 'users', method: RequestMethod.GET })
      // 或作用于整个控制器
      .forRoutes(UsersController)
      // 或通配所有路由
      .forRoutes({ path: '*', method: RequestMethod.ALL });
  }
}

排除特定路由:

typescript
consumer
  .apply(AuthMiddleware)
  .exclude(
    { path: 'auth/login', method: RequestMethod.POST },
    { path: 'auth/register', method: RequestMethod.POST },
    'health',  // 字符串形式
  )
  .forRoutes('*');

链式应用多个中间件(按顺序执行):

typescript
consumer
  .apply(CorsMiddleware, RateLimitMiddleware, LoggerMiddleware)
  .forRoutes('*');

函数式中间件

简单场景不需要 @Injectable(),直接用函数:

typescript
export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
  next();
}

// 在模块中注册
consumer.apply(logger).forRoutes('*');

// 或在 main.ts 全局注册(Express 原生方式)
app.use(logger);

中间件 vs 拦截器:何时用哪个?

场景推荐原因
给请求添加 traceIdMiddleware在路由解析之前就要设置
CORS 处理MiddlewareExpress 级别,影响所有请求
压缩(compression)MiddlewareExpress 中间件 compression
Cookie 解析Middlewarecookie-parser 中间件
接口耗时统计Interceptor需要包裹 Handler 前后
读取 @Roles() 装饰器Guard/InterceptorMiddleware 无法访问路由元数据

实战:请求追踪中间件

typescript
@Injectable()
export class TracingMiddleware implements NestMiddleware {
  use(
    req: Request & { traceId?: string },
    res: Response,
    next: NextFunction,
  ) {
    // 优先使用上游传入的 traceId(分布式追踪)
    req.traceId = (req.headers['x-trace-id'] as string) || randomUUID();
    res.setHeader('X-Trace-Id', req.traceId);
    next();
  }
}

二、Guard(守卫)

核心职责

守卫在 Middleware 之后、Pipe 之前执行。它的唯一职责是:决定当前请求是否允许继续

Guard 返回 true  → 请求继续
Guard 返回 false → NestJS 抛出 ForbiddenException(403)
Guard 抛出异常   → 该异常传播到 ExceptionFilter

ExecutionContext:请求上下文的统一抽象

ExecutionContext 是 NestJS 对不同传输层(HTTP、WebSocket、gRPC、Microservice)的统一抽象:

typescript
@Injectable()
export class ExampleGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    // HTTP 请求
    const httpRequest = context.switchToHttp().getRequest<Request>();
    const httpResponse = context.switchToHttp().getResponse<Response>();

    // WebSocket
    const wsClient = context.switchToWs().getClient();
    const wsData = context.switchToWs().getData();

    // gRPC
    const rpcData = context.switchToRpc().getData();

    // 获取当前处理方法和控制器类(用于读取元数据)
    const handler = context.getHandler();  // 方法引用
    const controller = context.getClass(); // 控制器类

    return true;
  }
}

JWT 认证守卫(完整实现)

typescript
@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(
    private jwtService: JwtService,
    private reflector: Reflector,
  ) {}

  canActivate(context: ExecutionContext): boolean {
    // 检查是否是公开接口(@Public() 装饰器)
    const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) return true;

    // 提取 Token
    const request = context.switchToHttp().getRequest<Request>();
    const token = this.extractToken(request);
    if (!token) throw new UnauthorizedException('缺少认证 Token');

    // 验证 Token
    try {
      const payload = this.jwtService.verify(token);
      request['user'] = payload;  // 将用户信息挂载到 request
      return true;
    } catch (err) {
      if (err.name === 'TokenExpiredError') {
        throw new UnauthorizedException('Token 已过期');
      }
      throw new UnauthorizedException('Token 无效');
    }
  }

  private extractToken(request: Request): string | null {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : null;
  }
}

角色守卫(读取自定义装饰器)

typescript
// 定义装饰器
export enum Role { USER = 'user', ADMIN = 'admin', MODERATOR = 'moderator' }
export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);

// 守卫实现
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles?.length) return true;  // 没有角色限制,放行

    const { user } = context.switchToHttp().getRequest();
    if (!user) throw new UnauthorizedException('未登录');

    const hasRole = requiredRoles.some(role => user.roles?.includes(role));
    if (!hasRole) throw new ForbiddenException(`需要 ${requiredRoles.join(' 或 ')} 权限`);

    return true;
  }
}

// 使用
@Roles(Role.ADMIN)
@Delete(':id')
remove(@Param('id') id: string) {}

异步守卫

守卫可以返回 Promise<boolean>Observable<boolean>

typescript
@Injectable()
export class PermissionGuard implements CanActivate {
  constructor(private permissionService: PermissionService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const { user } = context.switchToHttp().getRequest();
    const requiredPermission = this.reflector.get<string>(
      'permission',
      context.getHandler(),
    );

    // 异步查询数据库权限
    const hasPermission = await this.permissionService.check(
      user.id,
      requiredPermission,
    );

    if (!hasPermission) {
      throw new ForbiddenException(`缺少权限: ${requiredPermission}`);
    }
    return true;
  }
}

全局注册守卫(能注入依赖)

typescript
// ❌ main.ts 中注册无法注入 NestJS Provider
app.useGlobalGuards(new JwtAuthGuard());  // JwtService 无法注入

// ✅ 在 AppModule 中用 APP_GUARD 注册
@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,  // JwtService 可以正常注入
    },
    {
      provide: APP_GUARD,
      useClass: RolesGuard,    // 多个 APP_GUARD 按声明顺序执行
    },
  ],
})
export class AppModule {}

守卫 vs 中间件:核心区别

能力MiddlewareGuard
访问 req/res✅(通过 ExecutionContext)
读取路由装饰器元数据✅(通过 Reflector)
注入 NestJS Provider✅(@Injectable()
支持 WebSocket/gRPC✅(ExecutionContext 抽象)
返回 false 自动 403❌(需手动处理)
执行时机路由解析前路由解析后、Pipe 前

结论: 认证(验证 Token 有效性)和授权(检查权限)几乎总应该用 Guard,而不是 Middleware。Middleware 只用于与路由无关的通用处理。


可运行 Demo: practice/02-request-lifecycle — TracingMiddleware + TracingGuard 带时间戳追踪


常见错误

错误原因解决
中间件类未生效使用类中间件但未在 configure.apply()函数中间件可直接 use();类中间件必须在模块 configure 中注册
Guard 返回 false 但没有明确错误默认抛出 ForbiddenException可在 canActivate 中手动 throw new UnauthorizedException() 提供明确信息
ExecutionContext 拿不到 HTTP request在 WebSocket/RPC 场景使用了 HTTP 的 switchToHttp()context.getType() 判断后再切换

NestJS 深度学习体系