WEBKT

NestJS 中间件实战:请求拦截与处理的深度解析,附带权限验证、日志记录等场景示例

17 0 0 0

NestJS 中间件:你的 HTTP 请求守护神

1. 中间件是什么?

2. 如何在 NestJS 中使用中间件?

3. 中间件的执行顺序

4. 异步中间件

5. 实际应用场景

5.1 权限验证

5.2 日志记录

5.3 请求参数校验

6. 全局中间件 vs. 路由中间件

7. 总结

8. 进阶技巧和注意事项

9. 实践建议

10. 常见问题解答

NestJS 中间件:你的 HTTP 请求守护神

嘿,老铁!作为一名 NestJS 开发者,你是否经常遇到这样的需求:在处理每个请求之前,都需要进行用户身份验证、权限检查,或者记录请求日志?如果每次都在每个 Controller 里面写这些重复的代码,那简直要秃头了!

别担心,NestJS 中间件 (Middleware) 就是你的救星!它允许你在请求到达路由处理程序之前或之后,对请求进行拦截和处理,就像一个站在你 API 前面的守卫,帮你过滤、检查、修改请求,从而避免重复代码,保持代码的 DRY (Don't Repeat Yourself) 原则,让你的代码更优雅、更易于维护。

本文将带你深入了解 NestJS 中间件,包括它的基本概念、用法、执行顺序,以及在实际开发中的应用场景,比如权限验证、日志记录等,并提供详细的代码示例。让我们一起揭开中间件的神秘面纱吧!

1. 中间件是什么?

简单来说,中间件就是位于客户端和服务器之间的处理函数,它负责处理 HTTP 请求和响应。在 NestJS 中,中间件是一个函数,它接收三个参数:

  • req: Express 的请求对象,包含了客户端发送的请求信息。
  • res: Express 的响应对象,用于向客户端发送响应。
  • next: 一个函数,用于将控制权传递给下一个中间件或路由处理程序。如果你的中间件没有调用 next(),那么请求就会被卡在这里,服务器将无法响应。

2. 如何在 NestJS 中使用中间件?

在 NestJS 中,你可以使用 @Middleware 装饰器来定义中间件。下面是一个简单的例子:

// src/app.middleware.ts
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(`[${new Date().toISOString()}] Request: ${req.method} ${req.url}`);
next(); // 调用 next() 将控制权传递给下一个中间件或路由处理程序
}
}

在这个例子中,我们创建了一个名为 LoggerMiddleware 的中间件,它会在每个请求到达时,在控制台打印请求的日志信息。

接下来,你需要在你的模块中注册这个中间件。通常,我们会在根模块 AppModule 中注册中间件,如下所示:

// src/app.module.ts
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { LoggerMiddleware } from './app.middleware';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('*'); // 应用于所有路由
// .forRoutes({ path: 'cats', method: RequestMethod.GET }); // 仅应用于 /cats 路由的 GET 请求
}
}

AppModuleconfigure 方法中,我们使用 MiddlewareConsumer 来注册中间件。apply() 方法用于指定要使用的中间件,forRoutes() 方法用于指定中间件适用的路由。forRoutes('*') 表示将中间件应用于所有路由。你也可以使用对象字面量来指定更具体的路由,例如 { path: 'cats', method: RequestMethod.GET },表示仅应用于 /cats 路由的 GET 请求。

3. 中间件的执行顺序

中间件的执行顺序非常重要。NestJS 中间件的执行顺序取决于它们在模块中注册的顺序。当一个请求到达服务器时,NestJS 会按照中间件在 configure 方法中注册的顺序依次执行它们。当所有中间件都执行完毕后,才会执行路由处理程序。

如果多个中间件应用于同一个路由,它们会按照注册的顺序依次执行。如果某个中间件没有调用 next(),那么后续的中间件和路由处理程序将不会被执行。

4. 异步中间件

除了同步中间件之外,NestJS 还支持异步中间件。异步中间件使用 async/await 或者返回一个 Promise。在异步中间件中,你可以进行异步操作,例如数据库查询、API 调用等。

// src/auth.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { AuthService } from './auth.service';
@Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(private readonly authService: AuthService) {}
async use(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).send('Unauthorized');
}
try {
const user = await this.authService.validateToken(token);
if (user) {
req.user = user; // 将用户信息存储在请求对象中,方便后续使用
next();
} else {
return res.status(401).send('Unauthorized');
}
} catch (error) {
return res.status(401).send('Unauthorized');
}
}
}

在这个例子中,AuthMiddleware 是一个异步中间件,它用于验证用户身份。它从请求头中获取 Authorization 字段,然后调用 AuthServicevalidateToken 方法来验证 token。如果 token 有效,它会将用户信息存储在请求对象中,并调用 next() 继续处理请求。如果 token 无效,它会返回 401 状态码。

5. 实际应用场景

5.1 权限验证

权限验证是中间件最常见的应用场景之一。你可以使用中间件来检查用户是否具有访问特定资源的权限。

// src/roles.middleware.ts
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesMiddleware implements NestMiddleware {
constructor(private reflector: Reflector) {}
use(req: Request, res: Response, next: NextFunction) {
const roles = this.reflector.get<string[]>('roles', req.route);
if (!roles) {
return next(); // 如果没有指定角色,则放行
}
const user = req.user; // 从请求对象中获取用户信息,前提是已经通过身份验证中间件
if (!user) {
throw new UnauthorizedException();
}
const hasRole = () => roles.some((role) => user.roles?.includes(role));
if (hasRole()) {
return next();
}
throw new UnauthorizedException();
}
}

这个 RolesMiddleware 中间件使用 NestJS 的 Reflector 来获取路由处理程序上的角色信息。然后,它检查当前用户是否具有访问该路由所需的角色。如果没有,它会抛出一个 UnauthorizedException 异常。

要使用这个中间件,你需要在路由处理程序上使用 @Roles 装饰器来指定所需的角色:

// src/cats.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';
@Controller('cats')
export class CatsController {
@Get()
@Roles('admin', 'moderator') // 仅允许 admin 和 moderator 角色访问
@UseGuards(RolesGuard) // 使用 RolesGuard 检查角色
findAll() {
return 'This action returns all cats';
}
}
// src/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// src/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return roles.some((role) => user.roles?.includes(role));
}
}

5.2 日志记录

中间件也可以用于记录请求日志。你可以记录请求的 URL、方法、请求体、响应状态码等信息,方便你调试和监控你的 API。

// src/logging.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
const { method, url } = req;
res.on('finish', () => {
const duration = Date.now() - start;
const { statusCode } = res;
console.log(`[${new Date().toISOString()}] ${method} ${url} ${statusCode} - ${duration}ms`);
});
next();
}
}

这个 LoggingMiddleware 中间件会在每个请求到达时记录请求的开始时间,并在响应结束时记录请求的方法、URL、状态码和耗时。

5.3 请求参数校验

你可以使用中间件来校验请求参数。例如,你可以校验请求体中的数据是否符合预期格式。

// src/validation.middleware.ts
import { Injectable, NestMiddleware, BadRequestException } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
interface ValidationSchema {
new (...args: any[]): any;
}
@Injectable()
export class ValidationMiddleware implements NestMiddleware {
constructor(private readonly schema: ValidationSchema) {}
async use(req: Request, res: Response, next: NextFunction) {
try {
const object = plainToClass(this.schema, req.body);
const errors = await validate(object);
if (errors.length > 0) {
const message = errors
.map((error) => Object.values(error.constraints))
.flat()
.join(', ');
throw new BadRequestException(message);
}
req.body = object; // 将校验后的对象重新赋值给 req.body
next();
} catch (err) {
throw err;
}
}
}

这个 ValidationMiddleware 中间件使用 class-validatorclass-transformer 库来校验请求体中的数据。它接收一个 schema 参数,用于指定要校验的类。如果校验失败,它会抛出一个 BadRequestException 异常。

要使用这个中间件,你需要在模块中注册它,并传入相应的 schema

// src/cats.module.ts
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { ValidationMiddleware } from './validation.middleware';
import { CreateCatDto } from './create-cat.dto';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(ValidationMiddleware)
.forRoutes({ path: 'cats', method: RequestMethod.POST });
}
}
// src/create-cat.dto.ts
import { IsString, IsInt, Min } from 'class-validator';
export class CreateCatDto {
@IsString()
readonly name: string;
@IsInt()
@Min(0)
readonly age: number;
@IsString()
readonly breed: string;
}

在这个例子中,我们使用 ValidationMiddleware 来校验 /cats 路由的 POST 请求。CreateCatDto 定义了请求体的数据结构和校验规则。

6. 全局中间件 vs. 路由中间件

  • 全局中间件: 应用于所有路由。在 AppModuleconfigure 方法中使用 forRoutes('*') 来注册。适用于需要在所有请求中执行的逻辑,例如日志记录、身份验证等。
  • 路由中间件: 仅应用于特定的路由。在 AppModuleconfigure 方法中使用对象字面量来指定路由,例如 { path: 'cats', method: RequestMethod.GET }。适用于需要在特定路由中执行的逻辑,例如参数校验、权限验证等。

7. 总结

中间件是 NestJS 中一个非常强大的功能,它可以帮助你简化代码,提高代码的可维护性和可重用性。通过使用中间件,你可以轻松地实现权限验证、日志记录、参数校验等功能。希望本文能够帮助你更好地理解和使用 NestJS 中间件。记住,熟练掌握中间件是成为一名优秀 NestJS 开发者的必备技能之一!

8. 进阶技巧和注意事项

  • 错误处理: 在中间件中处理错误非常重要。你可以使用 try/catch 块来捕获错误,并返回适当的错误响应。如果错误无法在中间件中处理,你应该将错误传递给下一个中间件或路由处理程序。
  • 依赖注入: 可以在中间件中使用依赖注入。这允许你访问其他服务和模块,例如数据库连接、用户服务等。通过在中间件的构造函数中注入依赖,你可以方便地使用它们。
  • 中间件的顺序: 中间件的执行顺序非常重要。确保按照正确的顺序注册中间件,以满足你的业务需求。例如,身份验证中间件应该在权限验证中间件之前执行。
  • 测试中间件: 像测试其他代码一样,你应该测试你的中间件。使用单元测试来验证中间件的功能和逻辑。可以使用模拟请求和响应对象来模拟中间件的执行。
  • 性能优化: 中间件会对请求的性能产生一定的影响。尽量减少中间件中的复杂操作,例如数据库查询、API 调用等。如果中间件需要进行耗时的操作,可以考虑使用缓存或其他优化技术。
  • 第三方中间件: 除了自定义中间件之外,你还可以使用第三方中间件。例如,可以使用 cors 中间件来处理跨域请求,使用 helmet 中间件来提高安全性。

9. 实践建议

  1. 从简单开始: 从简单的中间件开始,例如日志记录或简单的请求头修改。逐步增加中间件的复杂性,以便更好地理解其工作原理。
  2. 阅读文档: 仔细阅读 NestJS 官方文档,了解中间件的详细信息和最佳实践。
  3. 参考示例: 查阅其他 NestJS 项目的中间件示例,学习其他开发者的经验。
  4. 测试你的中间件: 编写单元测试来验证你的中间件的功能,确保其正常工作。
  5. 保持代码简洁: 避免在中间件中编写过于复杂的逻辑。将复杂的功能分解成多个中间件,或者将它们移动到其他服务或模块中。

10. 常见问题解答

  • Q: 为什么我的中间件没有执行?
    • A: 检查你的中间件是否正确注册,以及 forRoutes() 方法中指定的路由是否正确。确保你的中间件没有调用 next(),导致请求被阻塞。
  • Q: 如何在中间件中获取请求体?
    • A: 你可以直接从 req.body 中获取请求体。但是,在使用请求体之前,你需要确保已经安装了 body-parser 中间件,并在你的 NestJS 应用中正确配置它。
  • Q: 如何在中间件中修改响应?
    • A: 你可以通过修改 res 对象来修改响应。例如,你可以设置响应头、状态码或发送响应体。
  • Q: 中间件和 Guard 的区别是什么?
    • A: 中间件在请求到达路由处理程序之前或之后执行,而 Guard 在路由处理程序之前执行。Guard 通常用于身份验证和授权,而中间件可以用于更广泛的任务,例如日志记录、参数校验等。Guard 也可以被视为一种特殊的中间件。

希望这篇文章能帮助你更好地理解和使用 NestJS 中间件!加油,老铁,祝你在 NestJS 的世界里越走越远!

前端老菜鸟 NestJS中间件请求拦截权限验证

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/7886