首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

卫士

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

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

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

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

授权守卫

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

auth.guard.ts

JS

代码语言:javascript
复制
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()函数内部的逻辑可以根据需要简单或复杂。该示例的重点是显示防护如何适应请求/响应周期。

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

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

执行上下文

canActivate()函数采用单个参数,即ExecutionContext实例。在ExecutionContext从继承ArgumentsHost。它ArgumentsHost是已传递给原始处理程序的参数的包装器,它根据应用程序的类型包含不同的参数数组。

代码语言:javascript
复制
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,但也提供了有关当前执行过程的更多详细信息。

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}

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

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

cats.controller.ts

JS

代码语言:javascript
复制
@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}

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

代码语言:javascript
复制
const app = await NestFactory.create(ApplicationModule);
app.useGlobalGuards(new RolesGuard());

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

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

app.module.ts

JS

代码语言:javascript
复制
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

代码语言:javascript
复制
@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

代码语言:javascript
复制
import { ReflectMetadata } from '@nestjs/common';

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

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

cats.controller.ts

JS

代码语言:javascript
复制
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

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

roles.guard.ts

JS

代码语言:javascript
复制
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

代码语言:javascript
复制
const roles = this.reflector.get<string[]>('roles', context.getClass());

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

代码语言:javascript
复制
{
  "statusCode": 403,
  "message": "Forbidden resource"
}

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

扫码关注腾讯云开发者

领取腾讯云代金券

http://www.vxiaotou.com