守卫

守卫是一个使用 @Injectable() 装饰器的类。 守卫应该实现 CanActivate 接口。

守卫 - 图1

守卫有一个单独的责任。它们根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理。这通常称为授权。在传统的 Express 应用程序中,通常由中间件处理授权(以及认证)。中间件是身份验证的良好选择,因为诸如 token 验证或添加属性到 request 对象上与特定路由(及其元数据)没有强关联。

中间件不知道调用 next() 函数后会执行哪个处理程序。另一方面,守卫可以访问 ExecutionContext 实例,因此确切地知道接下来要执行什么。它们的设计与异常过滤器、管道和拦截器非常相似,目的是让您在请求/响应周期的正确位置插入处理逻辑,并以声明的方式进行插入。这有助于保持代码的简洁和声明性。

守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。

授权守卫

正如前面提到的,授权是守卫的一个很好的用例,因为只有当调用者(通常是经过身份验证的特定用户)具有足够的权限时,特定的路由才可用。我们现在要构建的 AuthGuard 假设用户是经过身份验证的(因此,请求头附加了一个token)。它将提取和验证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() 函数。此函数应该返回一个布尔值,用于指示是否允许当前请求。它可以同步或异步地返回响应(通过 PromiseObservable)。Nest 使用返回值来控制下一个行为:

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

执行上下文

canActivate() 函数接收单个参数 ExecutionContext 实例。ExecutionContext 继承自 ArgumentsHost 。在异常过滤器章节,我们讲到过 ArgumentsHost。在上面的示例中,我们只是使用了之前在 ArgumentsHost上定义的帮助器方法来获得对请求对象的引用。有关此主题的更多信息。你可以在这里了解到更多(在异常过滤器章节)。

ExecutionContext 提供了更多功能,它扩展了 ArgumentsHost,但是也提供了有关当前执行进程的更多详细信息。这些细节有助于构建更通用的守卫,这些守卫可以在一系列的控制器、方法和执行上下文中工作。在这里了解有关 ExecutionContext 的更多信息。

基于角色认证

让我们构建一个功能更强大的守卫,它只允许具有特定角色的用户访问。我们将从一个基本的守卫模板开始,并在接下来的部分中以它为基础。目前,它允许所有请求通过:

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()装饰器设置了一个控制范围的守卫。这个装饰器可以使用单个参数,也可以使用逗号分隔的参数列表。也就是说,你可以传递几个守卫并用逗号分隔它们。

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

@UseGuards() 装饰器需要从 @nestjs/common 包导入。

上例,我们已经传递了 RolesGuard 类型而不是实例, 让框架进行实例化,并启用了依赖注入。与管道和异常过滤器一样,我们也可以传递一个实例:

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

上面的构造将守卫附加到此控制器声明的每个处理程序。如果我们希望守卫只应用于单个方法,则需在方法级别应用 @UseGuards() 装饰器。

为了设置一个全局守卫,使用Nest应用程序实例的 useGlobalGuards() 方法:

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

对于混合应用程序,默认情况下 useGlobalGuards() 方法不会为网关和微服务设置守卫(可查阅混合应用以了解如何改变此行为)。对于“标准”(非混合)微服务应用程序,useGlobalGuards() 在全局安装守卫。

全局守卫用于整个应用程序, 每个控制器和每个路由处理程序。在依赖注入方面, 从任何模块外部注册的全局守卫 (使用 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 AppModule {}

?> 当使用此方法为守卫程序执行依赖项注入时,请注意,无论使用此构造的模块是什么,守卫程序实际上是全局的。应该在哪里进行?选择定义守卫的模块(上例中的 RolesGuard)。此外,useClass不是处理自定义 providers 注册的唯一方法。在这里了解更多。

为每个处理器设置角色

我们的 RolesGuard 现在在正常工作,但还不是很智能。我们仍然没有利用最重要的守卫的特征,即执行上下文。它还不知道角色,或者每个处理程序允许哪些角色。例如,CatsController 可以为不同的路由提供不同的权限方案。其中一些可能只对管理用户可用,而另一些则可以对所有人开放。我们如何以灵活和可重用的方式将角色与路由匹配起来?

这就是自定义元数据发挥作用的地方(从这里了解更多)。Nest 提供了通过 @SetMetadata() 装饰器将定制元数据附加到路由处理程序的能力。这些元数据提供了我们所缺少的角色数据,而守卫需要这些数据来做出决策。让我们看看使用@SetMetadata():

cats.controller.ts

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

@SetMetadata() 装饰器需要从 @nestjs/common 包导入。

通过上面的构建,我们将 roles 元数据(roles 是一个键,而 ['admin'] 是一个特定的值)附加到 create() 方法。虽然这样可以运行,但直接使用 @SetMetadata() 并不是一个好做法。相反,你应该创建你自己的装饰器。

roles.decorator.ts

  1. import { SetMetadata } from '@nestjs/common';
  2. export const Roles = (...roles: string[]) => SetMetadata('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 中提供的 Reflector 帮助类。

roles.guard.ts

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

node.js 世界中,将授权用户附加到 request 对象是一种常见的做法。 因此,在上面的示例代码中。我们假设 request.user 包含用户实例和允许的角色。 在您的应用中,您可能会在自定义身份验证(或中间件)中建立该关联。查阅此处以了解更多。

matchRoles() 函数内部的逻辑可以根据需要简单或复杂。该示例的重点是显示防护如何适应请求/响应周期。

查阅执行上下文章节的[反射和元数据]部分,以了解如何以上下文相关(context-sensitive)的方式利用 Reflector

当权限不足的用户请求端点时,Nest 会自动返回以下响应:

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

其背后的原理是,当守卫返回 false 时,框架会抛出一个 ForbiddenException 异常。如果您想要返回不同的错误响应,你应该抛出一个你自己的准确声明的异常。

  1. throw new UnauthorizedException();

由守卫引发的任何异常都将由异常层(全局异常过滤器和应用于当前上下文的任何异常过滤器)处理。

如果你正在寻找有关如何实现授权的真实示例,请查看本章

译者署名

用户名 头像 职能 签名
@GODLiangCY 守卫 - 图2 翻译、校正 FE.@GODLiangCY