守卫 Guards

守卫是一个用 `@Injectable()` 装饰器注释的类,它实现了 `CanActivate` 接口。

img

Guard 具有单一职责。它们根据运行时存在的某些条件(如权限、角色、ACL 等)确定给定请求是否由路由处理程序处理。这通常称为授权。授权(及其通常与之协作的身份验证)通常由传统 Express 应用程序中的 中间件 处理。中间件是身份验证的不错选择,因为诸如令牌验证和将属性附加到 request 对象之类的事情与特定路由上下文(及其元数据)没有紧密联系。

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

提示

Guard 在所有中间件之后执行,但在任何拦截器或管道之前执行。

授权守卫

如上所述,授权 是 Guard 的一个很好的用例,因为只有当调用者(通常是特定的经过身份验证的用户)具有足够的权限时,特定路由才可用。我们现在将构建的 AuthGuard 假设一个经过身份验证的用户(因此,令牌附加到请求标头)。它将提取并验证令牌,并使用提取的信息来确定请求是否可以继续。

ts
TS
ts
// auth.guard
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { Observable } from 'rxjs'

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest()
    return validateRequest(request)
  }
}
提示

如果您正在寻找有关如何在应用程序中实现身份验证机制的真实示例,请访问本章。同样,有关更复杂的授权示例,请查看此页面

validateRequest() 函数中的逻辑可以根据需要简单或复杂。此示例的主要目的是展示守卫如何融入请求/响应周期。

每个守卫都必须实现一个 canActivate() 函数。此函数应返回一个布尔值,指示当前请求是否被允许。它可以同步或异步返回响应(通过 PromiseObservable)。Nest 使用返回值来控制下一个操作:

  • 如果返回 true,则将处理请求。
  • 如果返回 false,Nest 将拒绝该请求。

执行上下文

canActivate() 函数接受一个参数,即 ExecutionContext 实例。ExecutionContext 继承自 ArgumentsHost。我们之前在异常过滤器章节中看到过 ArgumentsHost。在上面的示例中,我们只是使用之前在 ArgumentsHost 上定义的相同辅助方法来获取对 Request 对象的引用。您可以参考 异常过滤器 章节的 Arguments host 部分,了解有关此主题的更多信息。

通过扩展 ArgumentsHostExecutionContext 还添加了几个新的辅助方法,这些方法提供有关当前执行过程的更多详细信息。这些详细信息有助于构建更通用的保护程序,这些保护程序可以跨广泛的控制器、方法和执行上下文工作。在此处 了解有关 ExecutionContext 的更多信息。

基于角色的身份验证

让我们构建一个功能更强大的防护器,仅允许具有特定角色的用户访问。我们将从一个基本的防护器模板开始,并在接下来的部分中对其进行构建。目前,它允许所有请求继续进行:

ts
TS
ts
// roles.guard
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { Observable } from 'rxjs'

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true
  }
}

绑定保护

与管道和异常过滤器一样,保护可以是控制器范围、方法范围或全局范围。下面,我们使用 @UseGuards() 装饰器设置控制器范围的保护。此装饰器可以采用单个参数或逗号分隔的参数列表。这样,您只需一个声明即可轻松应用适当的保护集。

ts
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
提示

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

上面,我们传递了 RolesGuard 类(而不是实例),将实例化的责任留给框架并启用依赖注入。与管道和异常过滤器一样,我们也可以传递一个就地实例:

ts
@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}

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

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

ts
const app = await NestFactory.create(AppModule)
app.useGlobalGuards(new RolesGuard())
通知

在混合应用的情况下,useGlobalGuards() 方法默认不会为网关和微服务设置保护(有关如何更改此行为的信息,请参阅混合应用)。对于标准(非混合)微服务应用,useGlobalGuards() 会全局安装保护。

全局保护用于整个应用,适用于每个控制器和每个路由处理程序。就依赖注入而言,从任何模块外部注册的全局保护(使用上述示例中的 useGlobalGuards())无法注入依赖项,因为这在任何模块的上下文之外完成。为了解决这个问题,您可以使用以下构造直接从任何模块设置保护:

app.module
ts
import { Module } from '@nestjs/common'
import { APP_GUARD } from '@nestjs/core'

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}
提示

使用此方法为保护器执行依赖注入时,请注意,无论使用此构造的模块是什么,保护器实际上都是全局的。应该在哪里执行此操作?选择定义保护器(上例中的RolesGuard)的模块。此外,useClass 不是处理自定义提供程序注册的唯一方法。了解更多信息 此处

设置每个处理程序的角色

我们的RolesGuard正在运行,但它还不是很智能。我们还没有利用最重要的保护功能 - 执行上下文。它还不知道角色,也不知道每个处理程序允许哪些角色。例如,CatsController 可以为不同的路由提供不同的权限方案。有些可能只对管理员用户可用,而其他可能对所有人开放。我们如何以灵活且可重复使用的方式将角色与路由匹配?

这就是自定义元数据发挥作用的地方(了解更多此处https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata))。 Nest 提供了通过Reflector#createDecorator静态方法创建的装饰器或内置的@SetMetadata()装饰器将自定义元数据附加到路由处理程序的功能。

例如,让我们使用Reflector#createDecorator方法创建一个@Roles()装饰器,它将元数据附加到处理程序。框架开箱即用的Reflector并从@nestjs/core包中公开。

roles.decorator
ts
import { Reflector } from '@nestjs/core'

export const Roles = Reflector.createDecorator<string[]>()

此处的Roles装饰器是一个接受单个string[]类型参数的函数。

现在,要使用此装饰器,我们只需用它来注释处理程序:

ts
TS
ts
// cats.controller
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

在这里,我们将 Roles 装饰器元数据附加到 create() 方法,表示只有具有 admin 角色的用户才应被允许访问此路由。

或者,我们可以使用内置的 @SetMetadata() 装饰器,而不是使用 Reflector#createDecorator 方法。详细了解 此处

将它们放在一起

现在让我们回过头来将它与我们的 RolesGuard 结合在一起。目前,它在所有情况下都简单地返回 true,允许每个请求继续进行。我们希望根据将 分配给当前用户 的角色与当前正在处理的路由所需的实际角色进行比较,使返回值具有条件性。为了访问路由的角色(自定义元数据),我们将再次使用 Reflector 辅助类,如下所示:

ts
TS
ts
// roles.guard
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { Roles } from './roles.decorator'

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get(Roles, context.getHandler())
    if (!roles) {
      return true
    }
    const request = context.switchToHttp().getRequest()
    const user = request.user
    return matchRoles(roles, user.roles)
  }
}
提示

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

警告

matchRoles() 函数中的逻辑可以根据需要简单或复杂。此示例的主要目的是展示保护如何适应请求/响应周期。

有关以上下文敏感的方式使用反射器的更多详细信息,请参阅执行上下文一章的反射和元数据部分。

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

ts
{
  "statusCode": 403,
  "message": "Forbidden resource",
  "error": "Forbidden"
}

请注意,在幕后,当守卫返回false时,框架会抛出ForbiddenException。如果您想返回不同的错误响应,则应抛出自己的特定异常。例如:

ts
throw new UnauthorizedException()

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

提示

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