NestJS09-Guards

214 阅读7分钟

守卫Guards需要用@Injectable()装饰器装饰,并且实现CanActivate接口。

守卫.png

守卫只有一个职责,就是判断这个路由是否可以被执行。这取决于某些特定的条件(比如 角色,授权等等)。这通常被称为授权。授权(经常和他的兄弟验证一起使用)在传统的Express里是用中间件来处理的。中间件对验证来说是不错的选择,因为像token验证和将属性附加到请求对象这样的事情与特定的路由上下文(及其元数据)没有紧密联系。

但是中间件并没有那么聪明,他不知道调用next()之后哪个路由将被执行。另一方面,守卫Guards通过ExecutionContext实例可以知道哪个路由将被执行。他的设计很像异常过滤器,管道,拦截器,可以让您在requestresponse的循环中植入特定的逻辑。这能减少大量的重复代码。

提示:
守卫的调用在中间件之后,在任何管道和拦截器之前。

授权守卫

在守卫中来授权是比较好的用例,因为只有当请求方(通常是经过身份验证的特定用户)拥有足够的权限时,特定的路由才可用。下面AuthGuard将会用来验证用户(因此token将会绑定到requestheaders中去),它会取出并验证这个token,并且使用这个信息来决定用户的请求能否访问这个路由。

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 this.validateRequest(request)
  }
}

validateRequest()函数中的逻辑可以根据需要简单或复杂。这个例子主要是告诉我们守卫怎么写在request/response周期中的。

每个守卫必须实现canActivate()方法。这个方法需要返回布尔值,来决定当前的请求是否被允许。他可以返回同步或者非同步的结果(利用Promise或者Observable).Nest用这个结果来控制下一个动作:

  • 如果返回true,这个请求将被允许
  • 如果返回false,这个请求会被拒绝

执行上下文

canActivate()方法有一个参数,它是ExecutionContext。它继承自ArgumentsHost,在异常过滤器章节中我们也用到了它,那个时候我们只用它来取的request对象。您可以回到exception filters这个章节再去看看。

通过拓展ArgumentsHostExecutionContext加了几个新的帮助方法,来提供一些附加的内容。这些内容可以更好的帮我们完成控制守卫和方法守卫的控制。想要了解更多ExecutionContext请看官网

基于角色的授权

让我们来创建一个有功能的守卫,来允许一些特定的用户才能访问接口。首先先创建一个基本的守卫,任何方法都放行,稍后会继续完善它的功能。

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;
  }
}

绑定守卫

类似于管道pipes,守卫可以是在控制器controller上或者方法上,又或是全局的。下面我们先用@UseGuards()装饰器来装饰一个控制器。这个装饰器可以传入一个参数,或者多个参数用逗号隔开。我这里展示的是一个参数的设置

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

当我们传入参数是类(替代实例)的时候,我们把创建的责任留给了框架并且允许的依赖注入,和管道pipes还有异常过滤器Exception filters一样,我们传入一个实例

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

这个做法,会对每个要进入Controller路由都有效,如果您只想针对某个方法,我们可以把@UseGuards()写在方法名上。

为了设置全局的守卫,可以在创建Nest实例的时候利用useGlobalGuards()方法。

对于混合应用程序,useGlobalGuards()方法默认不会为网关和微服务设置防护(有关如何更改此行为的信息,请参阅混合应用程序)https://docs.nestjs.com/faq/hybrid-application。对于“标准”(非混合)微服务应用程序,使用GlobalGuards()可以全局安装防护

全局守卫会使所有路由都要经过这个守卫。因为它是在Module外注册的守卫,所以不能利用框架的依赖注入。为了解决这个问题,你可以利用下面的方法。

import { RolesGuard } from './roles/roles.guard';
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
  imports: [CatsModule],
  controllers: [AppController],
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
    AppService,
  ],
})
export class AppModule {}
当使用这种方法为守卫执行依赖注入时,请注意,无论使用这种构造的模块是什么,保护实际上都是全局的。这应该在哪里进行?选择定义防护(在上面的示例中为RolesGuard)的模块。此外,useClass不是处理自定义提供程序注册的唯一方法。在这里(https://docs.nestjs.com/fundamentals/custom-providers)了解更多信息。

设置每个路由的角色

我们RolesGuard可以工作了,但是它现在还不是很聪明。我们还没有利用最重要的保护功能。它还不知道角色,和哪个角色可以被允许访问。例如,CatsController可能对不同的路由使用不同的权限方案。有些可能只有admin用户才能访问,另一些路由所有人都能访问。我们应该怎么匹配用户和路由,并且能重复利用这些配置。

这将利用自定义元数据(在这里看更多),Nest提供了通过@SetMetadata()修饰符将自定义元数据附加到路由处理程序的能力。该元数据提供了我们缺失的角色数据,智能守卫需要这些数据来做出决策。让我们看看使用@SetMetadata()

  constructor(private catsService: CatsService) {}
  @Post()
  @SetMetadata('roles', ['admin'])
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

我们将roles元数据(roles是key,['admin']是值)绑定到create()方法上。在方法上直接装饰,这不是用@SetMetadata()最好的方式,取而代之,我们可以创建一个自己的装饰器。

import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

这种做法会更加整洁并且易于阅读。先自我们有自定义的@Roles装饰器,我们可能用它装饰Create()方法。

  @Post()
  @Roles('admin')
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

把他们放在一起

现在让我们回过头来,把这个和我们的RolesGuard联系起来。目前,它只是在所有情况下返回true,允许每个请求继续。我们希望基于将分配给当前用户的角色与正在处理的当前路由所需的实际角色进行比较,使返回值具有条件。为了访问路由的角色(自定义元数据),我们将使用Reflector助手类,该类由框架提供,并从@nestjs/core包中公开。

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

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

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return this.matchRoles(roles, user.roles);
  }
}
在node.js世界中,通常的做法是将授权用户附加到请求对象。因此,在上面的示例代码中,我们假设request.user包含用户实例和允许的角色。在应用程序中,您可能会在自定义身份验证保护(或中间件)中进行关联。有关此主题的更多信息,请参阅https://docs.nestjs.com/security/authentication。

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

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

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

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

throw new UnauthorizedException();

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

本章代码

代码