中间件与守卫
一、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 拦截器:何时用哪个?
| 场景 | 推荐 | 原因 |
|---|---|---|
给请求添加 traceId | Middleware | 在路由解析之前就要设置 |
| CORS 处理 | Middleware | Express 级别,影响所有请求 |
| 压缩(compression) | Middleware | Express 中间件 compression |
| Cookie 解析 | Middleware | cookie-parser 中间件 |
| 接口耗时统计 | Interceptor | 需要包裹 Handler 前后 |
读取 @Roles() 装饰器 | Guard/Interceptor | Middleware 无法访问路由元数据 |
实战:请求追踪中间件
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 抛出异常 → 该异常传播到 ExceptionFilterExecutionContext:请求上下文的统一抽象
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 中间件:核心区别
| 能力 | Middleware | Guard |
|---|---|---|
访问 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() 判断后再切换 |