NestJS 中间件与守卫
Middleware、Guard、Interceptor、Pipe、Filter 的区别与实践
目录
执行顺序
请求 → Middleware → Guard → Interceptor(前) → Pipe → Handler → Interceptor(后) → Filter → 响应| 组件 | 职责 | 典型用例 |
|---|---|---|
| Middleware | 请求预处理 | 日志、CORS、Body 解析 |
| Guard | 访问控制 | 认证、授权、角色检查 |
| Interceptor | 请求/响应转换 | 日志、缓存、响应包装 |
| Pipe | 数据转换/验证 | DTO 验证、类型转换 |
| Filter | 异常处理 | 错误格式化、日志记录 |
Middleware
函数式中间件
typescript
import { Request, Response, NextFunction } from 'express';
export function loggerMiddleware(req: Request, res: Response, next: NextFunction) {
console.log(`[${req.method}] ${req.url}`);
next();
}
// 注册
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(loggerMiddleware)
.forRoutes('*');
}
}类式中间件
typescript
import { Injectable, NestMiddleware } from '@nestjs/common';
@Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(private readonly authService: AuthService) {}
use(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.split(' ')[1];
if (token) {
req['user'] = this.authService.validateToken(token);
}
next();
}
}
// 注册
consumer
.apply(AuthMiddleware)
.exclude({ path: 'health', method: RequestMethod.GET })
.forRoutes({ path: '*', method: RequestMethod.ALL });Guard
认证守卫
typescript
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly authService: AuthService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.split(' ')[1];
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
const user = await this.authService.validateToken(token);
request.user = user;
return true;
} catch {
throw new UnauthorizedException('Invalid token');
}
}
}角色守卫
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 {
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass()
]);
if (!requiredRoles) {
return true; // 无角色要求,放行
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some(role => user.roles?.includes(role));
}
}
// 使用
@Post()
@Roles('admin')
@UseGuards(AuthGuard, RolesGuard)
create() {}全局守卫
typescript
// main.ts
app.useGlobalGuards(new AuthGuard());
// 或通过模块注册(支持依赖注入)
@Module({
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard
}
]
})
export class AppModule {}Interceptor
日志拦截器
typescript
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const startTime = Date.now();
console.log(`→ ${request.method} ${request.url}`);
return next.handle().pipe(
tap(() => {
console.log(`← ${request.method} ${request.url} - ${Date.now() - startTime}ms`);
})
);
}
}响应转换拦截器
typescript
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(
map(data => ({
code: 0,
message: 'success',
data,
timestamp: Date.now()
}))
);
}
}缓存拦截器
typescript
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(private readonly cacheService: CacheService) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const cacheKey = `cache:${request.url}`;
const cached = await this.cacheService.get(cacheKey);
if (cached) {
return of(cached);
}
return next.handle().pipe(
tap(data => this.cacheService.set(cacheKey, data, 60))
);
}
}超时拦截器
typescript
import { timeout, catchError } from 'rxjs/operators';
import { TimeoutError, throwError } from 'rxjs';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
catchError(err => {
if (err instanceof TimeoutError) {
throw new RequestTimeoutException();
}
return throwError(() => err);
})
);
}
}Pipe
内置 Pipe
typescript
// 参数转换
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {}
// 默认值
@Get()
findAll(@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number) {}
// UUID 验证
@Get(':uuid')
findByUuid(@Param('uuid', ParseUUIDPipe) uuid: string) {}DTO 验证 Pipe
typescript
// main.ts
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 删除 DTO 中未定义的属性
forbidNonWhitelisted: true, // 存在未定义属性时抛出错误
transform: true, // 自动类型转换
transformOptions: {
enableImplicitConversion: true
}
}));
// create-user.dto.ts
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
@IsOptional()
@IsString()
name?: string;
}自定义 Pipe
typescript
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseDatePipe implements PipeTransform<string, Date> {
transform(value: string): Date {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new BadRequestException(`Invalid date: ${value}`);
}
return date;
}
}
// 使用
@Get()
findByDate(@Query('date', ParseDatePipe) date: Date) {}Exception Filter
全局异常过滤器
typescript
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
constructor(private readonly logger: LoggerService) {}
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception instanceof HttpException
? exception.message
: 'Internal server error';
const errorResponse = {
code: status,
message,
path: request.url,
timestamp: new Date().toISOString()
};
this.logger.error(`${request.method} ${request.url}`, exception);
response.status(status).json(errorResponse);
}
}HTTP 异常过滤器
typescript
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
response.status(status).json({
code: status,
message: typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message,
timestamp: Date.now()
});
}
}高频面试题
Q1: Middleware 和 Interceptor 的区别?
| 特性 | Middleware | Interceptor |
|---|---|---|
| 访问能力 | req/res 对象 | ExecutionContext |
| 依赖注入 | 类式中间件支持 | ✅ 完全支持 |
| 响应处理 | ❌ 无法访问响应体 | ✅ 可包装/转换响应 |
| 执行时机 | 路由匹配前 | Handler 前后 |
Q2: Guard 和 Middleware 的区别?
| 特性 | Guard | Middleware |
|---|---|---|
| 职责 | 访问控制(授权) | 请求预处理 |
| 返回值 | boolean(是否放行) | void(调用 next) |
| 元数据访问 | ✅ 可通过 Reflector | ❌ |
| 执行顺序 | 在 Middleware 之后 | 最先执行 |
Q3: 如何按顺序执行多个 Guard?
typescript
@UseGuards(AuthGuard, RolesGuard, PermissionsGuard)- 按声明顺序执行
- 任一 Guard 返回 false 或抛出异常,后续不执行
Q4: Pipe 的执行顺序?
- 全局 Pipe (
app.useGlobalPipes) - Controller 级 Pipe (
@UsePipes) - 方法级 Pipe (
@UsePipes) - 参数级 Pipe (
@Body(SomePipe))
最佳实践
typescript
// 推荐的组合使用方式
@Controller('users')
@UseGuards(AuthGuard) // Controller 级守卫
@UseInterceptors(LoggingInterceptor)
export class UsersController {
@Post()
@Roles('admin') // 方法级元数据
@UseGuards(RolesGuard) // 方法级守卫
@UsePipes(ValidationPipe) // 方法级管道
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}