Nest —— Guards

3,327 阅读9分钟

前言

       之前匆匆写了一篇关于Nest官文的翻译,然而写的时候比较着急,所以很多地方的翻译比较马虎,甚至直接丢进翻译器在丢出来....(有的时候我会被丢进翻译器之后被丢出来的译文吓到,原因是...译文比自己翻译的好/(ㄒoㄒ)/~~)但还是有很多部分是翻译器解决不了的,语句通顺固然优雅锦上添花,但一点小的错误却是致命的。
       现在手头比较悠闲,也打算重新修改一份比较优雅的中文文档。有人会问花这么多时间写这个东西,是不是真的有用。百度上面也有一些关于Nest的文档,完全也不会有人来看你写的翻译。我的感受是,可能这就是我的学习方式吧。其实平时阅读文档,大多数情况下都是脑子说会了,手说不会。一边翻译英文文档,一遍理解框架的含义,还有助于提高阅读英文文档的能力。手敲过一遍和眼睛看过一遍真的不太一样,而且这种方式会增加使用时的自信。之后打算将每一章节分开书写,最后再通过链接汇总到一篇Nest妲己大记中去。就算没有人看,自己想要翻阅文档的时候,也可以拿出来看看,还能改改。这是一种乐趣,就好像养成游戏一样。



正文 Guards

guard守卫是一个使用装饰器 @Injectable() 的类,它应当实现 CanActivate 接口

守卫只有一个职责。他们决定了请求是否要被路由处理程序处理,取决于运行时某些情况(例如权限,角色,ACL访问控制列表)。这些通常被称作授权。在传统的Express应用中,权限通常被中间件来处理。对于权限控制,中间件确实是一个好的选择,因为一些像令牌验证或者获取请求对象的属性的操作,和特定的路由上下文(包括元数据)并没有太大的联系。
但是中间件是沉默性质的。他不知道那个路由在调用 next() 函数后哪一个处理程序将被执行。然而,守卫可以访问 ExecutionContext 实例,就能够准确地知道下一个将执行的是什么。我们将他设计得像异常过滤器、管道、拦截器,让你可以在请求响应周期中,位于正确的点上精确地插入一些处理逻辑。

Hints
守卫会在每个中间件后被执行,但是在任何拦截器和管道之前。



授权守卫

正如所提及的,授权是守卫很好的一个使用场景,因为某些特定的路由仅仅在拥有的足够的权限(通常是一个特定的被授权的用户)时候才可以访问。我们构建的 AuthGuard mock了一个被授权的用户。他将提取并校验令牌,并且使用提取出来的信息来决定是否请求会被放行。

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

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

validateRequest() 函数中的逻辑可简单可复杂按需而来。这个例子主要展示了守卫在请求响应循环中如何工作。
每个守卫必修实现一个 canActivate() 函数。这个函数应该返回一个布尔值,表示当前请求是否被允许,可以同步或者异步地返回结果(通过Promise,或者Observable)。Nest使用返回值来控制以下操作。

  • 如果返回true,请求会被处理
  • 如果返回false,Nest将会拒绝这个请求。



Execution Context 执行上下文

canActivate() 函数接受一个参数: ExecutionContext 实例。 ExecutionContext 继承自 ArgumentsHost。 我们只是使用了同一个定义在 ArugementsHost 上的辅助函数来获取请求对象。

通过扩展 ArgumentsHostExecutionContext 也添加了一些新的辅助函数来提供关于当前执行上下文的额外的信息。这些详情在构建更通用的守卫时是很有用的,可以跨多个控制器、方法、执行上下文工作。



Role-based authentication 基于角色的授权

让我们来构建一个更实用的守卫,它只允许一个特定角色访问。目前它会放行所有请求。

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

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



Building guards 构建守卫

就像管道和异常过滤器,守卫可以是控制器范围、方法范围或者全局范围的。下面我们使用 @UseGuards() 装饰器来设置一个控制器范围的守卫。这个装饰器接受一个参数,或者逗号分隔的参数列表。这可以让你简单地使用一个声明来应用多个守卫。

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

Hints
import { UseGuards } from '@nestjs/common

以上,我们传入 RolesGuard 类型(而不是实例),将实例化的工作交给框架并实现依赖注入。也可以就地传入实例

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

以上示例可以为控制器声明的每一个路由处理程序附加守卫。如果我们希望守卫应用在单独的方法上,我们可以在方法层级应用 @UseGuards()

要设置全局守卫,可以再应用实例上使用 @UseGlobalGuards() 方法。

const app = await NestFactory.create(AppModule)
app.useGlobalGuards(new RolesGuards())

NOTICE
在混合应用中, useGlobalGuards() 方法不会为网关和微服务设置守卫。标准(非混合)的微服务应用 useGlobalGuards() 方法不会挂载全局的守卫。


全局守卫在整个应用中被使用,每个控制器每个路由处理程序。按照依赖注入,注册在任何模块之外的全局守卫不能注入依赖,因为这是在模块上下文之外完成的。为了解决这个问题,你可以如下设置守卫。

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

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

Hints
当你使用这种方法来为守卫实现依赖注入,注意无论模块的构造器在哪里调用,是加上守卫都是全局。选择守卫定义的模块就好。useClass也不是处理自定义provider注册的唯一的方法。



Setting roles per handler 为每个处理程序设置角色

虽然 RolesGuard 已经正常工作,但是还不是很智能。我们还没有利用守卫最重要的特性——执行上下文。他现在还不知道关于角色的事情,或者在每个路由上哪个角色会被允许通过。 CatsController 的每个路不用的路由可以拥有不同的权限模型。其中一些可能只允许一个管理员用户访问,剩下其他的每个人都可以打开。我们如何用灵活和可重用的方式将觉得匹配到路由呢。

这就是自定义元数据发挥的作用。Nest提供了通过使用装饰器 @SetMetadata() 来将自定义元数据附加在路由处理程序上的能力。元数据提供了我们缺少的一个智能的守卫来做决定需要的角色信息。

// cats.controller.ts
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto)
}

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

如上,我们将角色元数据(roles是键,['admin']是一值)附加到 create() 方法。不过当他工作是时,在路由里这样去设置不是很优雅,可以使用自定义装饰器像下面这样

// roles.decarator.ts
import { SetMetadata } from '@nestjs/common'

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

这个方式更加简洁可读。现在我们应用在 create() 方法中

// cats.controller.ts
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catService.create(createCatDto)
}



将他们放在一起

现在我们回头将他与我们的 RolesGuard 联系起来。目前守卫在所有情况下都返回true,允许所有请求放行。我们希望在当前处理的路由中,使返回值基于比较分配给当前用户的角色和实际所需要的角色。为了获取到路由上角色的信息,我们使用 Reflector 辅助类。(开箱即用,由@nestjs/core提供)

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

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly 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 matchRoles(roles, user.roles)
  }
}

Hints
在node.js的世界,将授权用户添加到请求对象上是很常见的。因此,在我们的上面的例子中,我们假设request.user包含了用户的实例和被允许的用户。在你的应用中,你可能 会在你的自定义授权守卫(或者中间件)中做这件事情。

NOTICE
matchRoles() 方法中的逻辑可简单可复杂按需而来。


当一个权限不足的用户请求这个节点,Nest会自动返回一下响应

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

注意下面的场景,当守卫返回false,框架会抛出一个 ForbiddenException 异常。如果你希望返回一个不同的错误响应,你应该抛出你自己的特定的异常。

throw new UnauthorizedException()

任何通过守卫抛出的异常会被异常层处理。(全局异常过滤器,任何异常过滤器都会在当前上下文被应用)





后记

原文地址: docs.nestjs.com/guards


关于本文

  • 文章非复制黏贴,经浏览文档,以自己的理解进行,代码测试,手打书写。本篇为翻译+意译。
  • 用作记录自己曾经学习、思考过的问题的一种笔记。
  • 用作前端技术交流分享。
  • 阅读本文时欢迎随时质疑本文的准确性,将错误的地方告诉我。本人会积极修改,避免文章对读者的误导。

关于我

  • 是一只有梦想的肥柴。
  • 觉得算法、数据结构、函数式编程、js底层原理等十分有趣的小前端。
  • 志同道合的朋友请关注我,一起交流技术,在前端之路上共同成长。
  • 如对本人有任何意见建议尽管告诉我哦~ 初为肥柴,请多多关照~
  • 前端路漫漫,技术学不完。今天也是美(diao)好(fa)的一天( 跪了...orz