WEBKT

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

221 0 0 0

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中间件请求拦截权限验证

评论点评