在线文档教程

卫士

卫士

警卫是一个用@Injectable()装饰者注释的类。警卫应该实现CanActivate界面。

卫兵负有单一责任。它们确定请求是否应由路由处理程序处理。到目前为止,访问限制逻辑主要位于中间件内部。它仍然很好,因为诸如令牌验证或附加属性的request事物与特定路线没有很强的联系。

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

提示保护在每个中间件之后执行,但任何管道之前执行。

授权守卫

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

auth.guard.ts

JS

import { Injectable, CanActivate, ExecutionContext } 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()函数内部的逻辑可以根据需要简单或复杂。该示例的重点是显示防护如何适应请求/响应周期。

  • 如果它返回true,将处理用户调用。

执行上下文

export interface ArgumentsHost { getArgs<T extends Array<any> = any[]>(): T; getArgByIndex<T = any>(index: number): T; switchToRpc(): RpcArgumentsHost; switchToHttp(): HttpArgumentsHost; switchToWs(): WsArgumentsHost; }

ArgumentsHost一组有用的方法,有助于从底层阵列挑选正确的参数为我们提供。换句话说,ArgumentsHost只不过是一个参数数组。例如,当在HTTP应用程序上下文中使用guard时,ArgumentsHost将包含[request, response]数组。但是,当前上下文是Web套接字应用程序时,此数组将等于[client, data]。此设计决策使您可以访问最终将传递给相应处理程序的任何参数。

ExecutionContext提供多一点点。它扩展了ArgumentsHost,但也提供了有关当前执行过程的更多详细信息。

export interface ExecutionContext extends ArgumentsHost { getClass<T = any>(): Type<T>; getHandler(): Function; }

所述getHandler()返回一个参考当前处理的处理程序,而getClass()返回的类型Controller此特定处理程序属于类别。使用换句话说,如果用户指向create()方法被定义和内注册CatsController时,getHandler()将返回一个参考create()方法和getClass()在这种情况下,将简单地返回一个CatsController类型(未实例)。

基于角色的身份验证

一个更详细的例子是一个RolesGuard。此保护仅允许具有特定角色的用户访问。我们将从一个基本的防护模板开始,目前,它允许所有请求继续进行:

roles.guard.ts

JS

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

结合守卫

防护可以是控制器作用域,方法作用域和全局作用域。为了设置守卫,我们必须使用@UseGuards()装饰器。这个装饰器可能需要无数个参数,这意味着你可以传递几个守卫并用逗号分隔它们。

cats.controller.ts

JS

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

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

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

cats.controller.ts

JS

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

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

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

注意useGlobalGuards()方法不为网关和微服务设置保护。

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

app.module.ts

JS

import { Module } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; @Module{ providers: [ { provide: APP_GUARD, useClass: RolesGuard, }, ], }) export class ApplicationModule {}

提示替代选项是使用执行上下文功能。此外,useClass这不是处理自定义提供程序注册的唯一方法。在这里了解更多。

反光

守卫现在正在工作,但我们仍然没有利用最重要的防守功能,即执行环境

RolesGuard到目前为止,这是不可重用的。我们如何知道处理程序需要处理哪些角色?在CatsController可能有很多。有些可能只适用于管理员,有些可供所有人使用,但是,它们没有任何权限。

这就是为什么除了守卫之外,Nest还提供了通过装饰器附加自定义元数据的能力@ReflectMetadata()

cats.controller.ts

JS

@Post() @ReflectMetadata('roles', ['admin']) async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto }

提示@ReflectMetadata()装饰器从导入的@nestjs/common包。

通过上面的构造,我们为方法附加了roles元数据(roles是一个键,同时['admin']是一个特定的值)create()@ReflectMetadata()直接使用不是一个好习惯。相反,您应该始终创建自己的装饰器:

roles.decorator.ts

JS

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

这种方法更清晰,更易读。既然我们@Roles()现在有了装饰器,我们就可以将它与create()方法一起使用。

cats.controller.ts

JS

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

好的。让我们RolesGuard再回过头来。它只是true立即返回,允许请求继续进行到目前为止。为了反映元数据,我们将使用Reflector框架提供的并从@nestjs/core包中公开的辅助类。

roles.guard.ts

JS

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Observable } from 'rxjs'; 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; const hasRole = () => user.roles.some((role) => roles.includes(role) return user && user.roles && hasRole( } }

提示在node.js世界中,将授权用户附加到request对象是一种常见做法。这就是我们假设request.user包含用户实例的原因。

Reflector使我们能够轻松地反映了指定的元数据的关键。在上面的示例中,我们使用getHandler()以反映元数据,因为它是对路由处理函数的引用。如果我们添加控制器反射部分,我们可以使这个防护更加通用。要提取控制器元数据,我们应该使用context.getClass()而不是getHandler()函数:

JS

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

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

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

事实上,回归false投掷的后卫HttpException。如果要向最终用户返回不同的错误响应,则应抛出异常。之后,异常过滤器可以捕获此异常。