看守器

看守器是一个使用 @Guard() 装饰器的类。 看守器应该使用 CanActivate 接口。

看守器 - 图1

看守器有一个单独的责任。它们确定请求是否应该由路由处理程序处理。到目前为止, 访问限制逻辑大多在中间件内。这样很好, 因为诸如 token 验证或将req对象附加属性与特定路由没有强关联。

但中间件是非常笨的。它不知道调用 next() 函数后应该执行哪个处理程序。另一方面, 看守器可以访问 ExecutionContext 对象, 所以我们确切知道将要评估什么。

?> 看守器是在每个中间件之后执行的, 但在管道之前。

授权看守器

最好的用例之一就是认证逻辑,因为只有当调用者具有足够的权限(例如管理员角色)时才能使用特定的路由。我们有一个计划要创建的 AuthGuard 将依次提取和验证在请求标头中发送的 token。

auth.guard.ts

  1. import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
  2. import { Observable } from 'rxjs';
  3. @Injectable()
  4. export class AuthGuard implements CanActivate {
  5. canActivate(
  6. context: ExecutionContext,
  7. ): boolean | Promise<boolean> | Observable<boolean> {
  8. const request = context.switchToHttp().getRequest();
  9. return validateRequest(request);
  10. }
  11. }

不管 validateRequest() 函数背后的逻辑是什么, 主要的一点是要展示如何简单地利用看守器。每个看守器都提供一个 canActivate() 功能。看守器可能通过 (Promise 或 Observable) 同步地或异步地返回它的布尔答复。返回的值控制 Nest 行为:

  • 如果返回 true, 将处理用户调用。
  • 如果返回 false, 则 Nest 将忽略当前处理的请求。

canActivate() 函数采用单参数 ExecutionContext 实例。ExecutionContext 从 ArgumentsHost 继承 (这里首先提到)。ArgumentsHost 是围绕已传递给原始处理程序的参数的包装, 它包含基于应用程序类型的引擎下的不同参数数组。

  1. export interface ArgumentsHost {
  2. getArgs<T extends Array<any> = any[]>(): T;
  3. getArgByIndex<T = any>(index: number): T;
  4. switchToRpc(): RpcArgumentsHost;
  5. switchToHttp(): HttpArgumentsHost;
  6. switchToWs(): WsArgumentsHost;
  7. }

ArgumentsHost 为我们提供了一套有用的方法, 帮助从基础数组中选取正确的参数。换言之, ArgumentsHost 只是一个参数数组而已。例如, 当在 HTTP 应用程序上下文中使用该保护程序时, ArgumentsHost 将在内部包含 [request, response] 数组。但是, 当当前上下文是 web 套接字应用程序时, 此数组将等于 [client, data]]。通过此设计, 您可以访问最终传递给相应处理程序的任何参数。

ExecutionContext 提供多一点。它扩展了 ArgumentsHost, 而且还提供了有关当前执行过程的更多细节。

getHandler() 返回对当前处理的处理程序的引用, 而 getClass() 返回此特定处理程序所属的控制器类的类型。换句话说, 如果用户指向在 CatsController 中定义和注册的 create() 方法, 则 getHandler() 将返回对 create() 方法和 getClass() 的引用, 在这种情况下, 将只返回一个 CatsController 类型 (不是实例)。

基于角色的认证

一个更详细的例子是一个 RolesGuard 。这个看守器只允许具有特定角色的用户访问。我们要从一个基本的看守器模板开始:

roles.guard.ts

  1. import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
  2. import { Observable } from 'rxjs';
  3. @Injectable()
  4. export class RolesGuard implements CanActivate {
  5. canActivate(
  6. context: ExecutionContext,
  7. ): boolean | Promise<boolean> | Observable<boolean> {
  8. return true;
  9. }
  10. }

看守器可以是控制器范围的,方法范围的和全局范围的。为了建立看守器,我们使用 @UseGuards() 装饰器。这个装饰器可以带来无数的参数。也就是说,你可以传递几个看守器并用逗号分隔它们。

cats.controller.ts

  1. @Controller('cats')
  2. @UseGuards(RolesGuard)
  3. export class CatsController {}

!> @UseGuards() 装饰器是从 @nestjs/common 包中导入的。

我们已经通过了 RolesGuard 类型而不是实例, 使框架成为实例化责任并启用依赖项注入。另一种可用的方法是传递立即创建的实例:

cats.controller.ts

  1. @Controller('cats')
  2. @UseGuards(new RolesGuard())
  3. export class CatsController {}

上面的构造将守卫附加到此控制器声明的每个处理程序。如果我们决定只限制其中一个, 我们只需要设置在方法级别的看守器。为了绑定全局看守器, 我们使用 Nest 应用程序实例的 useGlobalGuards() 方法:

  1. const app = await NestFactory.create(ApplicationModule);
  2. app.useGlobalGuards(new RolesGuard());

!> 该 useGlobalGuards() 方法没有设置网关和微服务的看守器。

全局看守器用于整个应用程序, 每个控制器和每个路由处理程序。在依赖注入方面, 从任何模块外部注册的全局看守器 (如上面的示例中所示) 不能插入依赖项, 因为它们不属于任何模块。为了解决此问题, 您可以使用以下构造直接从任何模块设置一个看守器:

app.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { APP_GUARD } from '@nestjs/core';
  3. @Module({
  4. providers: [
  5. {
  6. provide: APP_GUARD,
  7. useClass: RolesGuard,
  8. },
  9. ],
  10. })
  11. export class ApplicationModule {}

?> 另一种选择是使用执行上下文功能。另外,useClass并不是处理自定义提供商注册的唯一方法。在这里了解更多

反射器

看守器在正常工作,但我们仍然没有利用最重要的看守器的特征,即执行上下文。

现在,RolesGuard 是不可重用的。 我们如何知道处理程序需要处理哪些角色? CatsController 可以有很多。 有些可能只适用于管理员,一些适用于所有人。

这就是为什么与看守器一起,Nest 提供了通过 @ReflectMetadata() 装饰器附加自定义元数据的能力。

cats.controller.ts

  1. @Post()
  2. @ReflectMetadata('roles', ['admin'])
  3. async create(@Body() createCatDto: CreateCatDto) {
  4. this.catsService.create(createCatDto);
  5. }

!> @ReflectMetadata() 装饰器是从 @nestjs/common 包中导入的。

通过上面的构建,我们将 roles 元数据(roles 是一个关键,虽然[‘admin’]是一个特定的值)附加到 create() 方法。 直接使用 @ReflectMetadata() 并不是一个好习惯。 相反,你应该总是创建你自己的装饰器。

roles.decorator.ts

  1. import { ReflectMetadata } from '@nestjs/common';
  2. export const Roles = (...roles: string[]) => ReflectMetadata('roles', roles);

这样更简洁。 由于我们现在有一个 @Roles() 装饰器,所以我们可以在 create() 方法中使用它。

cats.controller.ts

  1. @Post()
  2. @Roles('admin')
  3. async create(@Body() createCatDto: CreateCatDto) {
  4. this.catsService.create(createCatDto);
  5. }

我们再来关注一下 RolesGuard 。 现在,它立即返回 true ,允许请求继续。 为了反映元数据,我们将使用在 @nestjs/core 中提供的反射器 helper 类。

roles.guard.ts

  1. import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
  2. import { Observable } from 'rxjs';
  3. import { Reflector } from '@nestjs/core';
  4. @Injectable()
  5. export class RolesGuard implements CanActivate {
  6. constructor(private readonly reflector: Reflector) {}
  7. canActivate(context: ExecutionContext): boolean {
  8. const roles = this.reflector.get<string[]>('roles', context.getHandler());
  9. if (!roles) {
  10. return true;
  11. }
  12. const request = context.switchToHttp().getRequest();
  13. const user = request.user;
  14. const hasRole = () => user.roles.some((role) => roles.includes(role));
  15. return user && user.roles && hasRole();
  16. }
  17. }

?> 在 node.js 世界中,将授权用户附加到 request 对象是一种常见的做法。 这就是为什么我们假定 request.user 包含用户对象。

反射器允许我们通过指定的键很容易地反映元数据。 在上面的例子中,我们反映了处理程序,因为它是对路由处理函数的引用。 如果我们也添加控制器反射部分,我们可以使这个警卫更通用。 为了提取控制器元数据,我们只是使用 context.getClass() 而不是 getHandler()函数。

  1. const roles = this.reflector.get<string[]>('roles', context.getClass());

现在,当用户尝试调用没有足够权限的 /cat POST 端点时,Nest 会自动返回以下响应:

  1. {
  2. "statusCode": 403,
  3. "message": "Forbidden resource"
  4. }

实际上,返回 false 的守护器强制 Nest 抛出一个 HttpException 异常。如果您想要向最终用户返回不同的错误响应,则应该引发异常.这个异常可以被异常过滤器捕获。