🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## 授权(Authorization) 授权是指确定一个用户可以做什么的过程。例如,管理员用户可以创建、编辑和删除文章,非管理员用户只能授权阅读文章。 授权和认证是相互独立的。但是授权需要依赖认证机制。 有很多方法和策略来处理权限。这些方法取决于其应用程序的特定需求。本章提供了一些可以灵活运用在不同需求条件下的权限实现方式。 ### 基础的RBAC实现 基于角色的访问控制(**RBAC**)是一个基于角色和权限等级的中立的访问控制策略。本节通过使用`Nest`[守卫](https://docs.nestjs.com/guards)来实现一个非常基础的`RBAC`。 首先创建一个`Role`枚举来表示系统中的角色: > role.enum.ts ```TypeScript export enum Role { User = 'user', Admin = 'admin', } ``` > 在更复杂的系统中,角色信息可能会存储在数据库里,或者从一个外部认证提供者那里获取。 有了这个,我们可以创建一个`@Roles()`的装饰器,该装饰器允许某些角色拥有获取特定资源访问权。 > roles.decorator.ts ```TypeScript import { SetMetadata } from '@nestjs/common'; import { Role } from '../enums/role.enum'; export const ROLES_KEY = 'roles'; export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles); ``` 现在可以将`@Roles()`装饰器应用于任何路径处理程序。 >cats.controller.ts ```TypeScript @Post() @Roles(Role.Admin) create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); } ``` 最后,我们创建一个`RolesGuard`类来比较当前用户拥有的角色和当前路径需要的角色。为了获取路径的角色(自定义元数据),我们使用`Reflector`辅助类,这是个`@nestjs/core`提供的一个开箱即用的类。 > roles.guard.ts ```TypeScript 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 requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); if (!requiredRoles) { return true; } const { user } = context.switchToHttp().getRequest(); return requiredRoles.some((role) => user.roles?.includes(role)); } } ``` > 参见[应用上下文](https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata)章节的反射与元数据部分,了解在上下文敏感的环境中使用`Reflector`的细节。 > 该例子被称为“基础的”,是因为我们仅仅在路径处理层面检查了用户权限。在实际项目中,你可能有包含不同操作的终端/处理程序,它们各自需要不同的权限组合。在这种情况下,你可能要在你的业务逻辑中提供一个机制来检查角色,这在一定程度上会变得难以维护,因为缺乏一个集中的地方来关联不同的操作与权限。 在这个例子中,我们假设`request.user`包含用户实例以及允许的角色(在`roles`属性中)。在你的应用中,需要将其与你的认证守卫关联起来,参见[认证](#认证(Authentication))。 要确保该示例可以工作,你的`User`类看上去应该像这样: ```TypeScript class User { // ...other properties roles: Role[]; } ``` 最后,在控制层或者全局注册`RolesGuard`。 ```TypeScript providers: [ { provide: APP_GUARD, useClass: RolesGuard, }, ], ``` 当一个没有有效权限的用户访问一个终端时,Nest自动返回以下响应: ```JSON { "statusCode": 403, "message": "Forbidden resource", "error": "Forbidden" } ``` > 如果你想返回一个不同的错误响应,你应该抛出你自己的特定异常而不是返回一个布尔值。 ### 基于权利(Claims)的权限 一个身份被创建后,可能关联来来自信任方的一个或者多个权利。权利是指一个表示对象可以做什么,而不是对象是什么的键值对。 要在Nest中实现基于权利的权限,你可以参考我们在`RBAC`部分的步骤,仅仅有一个显著区别:比较`许可(permissions)`而不是角色。每个用户应该被授予了一组许可,相似地,每个资源/终端都应该定义其需要的许可(例如通过专属的`@RequirePermissions()`装饰器)。 > cats.controller.ts ```TypeScript @Post() @RequirePermissions(Permission.CREATE_CAT) create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); } ``` > 在这个例子中,`Permission`(和RBAC部分的`角色`类似)是一个TypeScript的枚举,它包含了系统中所有的许可。 ### 与`CASL`集成 `CASL`是一个权限库,用于限制用户可以访问哪些资源。它被设计为可渐进式增长的,从基础权利权限到完整的基于主题和属性的权限都可以实现。 首先,安装`@casl/ability`包: ```bash $ npm i @casl/ability ``` > 在本例中,我们选择`CASL`,但也可以根据项目需要选择其他类似库例如`accesscontrol`或者`acl`。 安装完成后,为了说明CASL的机制,我们定义了两个类实体,`User`和`Article`。 ```TypeScript class User { id: number; isAdmin: boolean; } ``` `User`类包含两个属性,`id`是用户的唯一标识,`isAdmin`代表用户是否有管理员权限。 ```TypeScript class Article { id: number; isPublished: boolean; authorId: number; } ``` `Article`类包含三个属性,分别是`id`、`isPublished`和`authorId`,`id`是文章的唯一标识,`isPublished`代表文章是否发布,`authorId`代表发表该文章的用户id。 接下来回顾并确定本示例中的需求: - 管理员可以管理(创建、阅读、更新、删除/CRUD)所有实体 - 用户对所有内容有阅读权限 - 用户可以更新自己的文章(`article.authorId===userId`) - 已发布的文章不能被删除 (`article.isPublised===true`) 基于这些需求,我们开始创建`Action`枚举,包含了用户可能对实体的所有操作。 ```TypeScript export enum Action { Manage = 'manage', Create = 'create', Read = 'read', Update = 'update', Delete = 'delete', } ``` > `manage`是CASL的关键词,代表`任何`操作。 要封装CASL库,需要创建`CaslModule`和`CaslAbilityFactory`。 ```bash $ nest g module casl $ nest g class casl/casl-ability.factory ``` 创建完成后,在`CaslAbilityFactory`中定义`createForUser()`方法。该方法将为用户创建`Ability`对象。 ```TypeScript type Subjects = InferSubjects<typeof Article | typeof User> | 'all'; export type AppAbility = Ability<[Action, Subjects]>; @Injectable() export class CaslAbilityFactory { createForUser(user: User) { const { can, cannot, build } = new AbilityBuilder< Ability<[Action, Subjects]> >(Ability as AbilityClass<AppAbility>); if (user.isAdmin) { can(Action.Manage, 'all'); // read-write access to everything } else { can(Action.Read, 'all'); // read-only access to everything } can(Action.Update, Article, { authorId: user.id }); cannot(Action.Delete, Article, { isPublished: true }); return build({ // Read https://casl.js.org/v5/en/guide/subject-type-detection#use-classes-as-subject-types for details detectSubjectType: item => item.constructor as ExtractSubjectType<Subjects> }); } } ``` > `all`是CASL的关键词,代表`任何对象`。 > `Ability`,`AbilityBuilder`,和`AbilityClass`从`@casl/ability`包中导入。 在上述例子中,我们使用`AbilityBuilder`创建了`Ability`实例,如你所见,`can`和`cannot`接受同样的参数,但代表不同含义,`can`允许对一个对象执行操作而`cannot`禁止操作,它们各能接受4个参数,参见[CASL文档](https://casl.js.org/v4/en/guide/intro)。 最后,将`CaslAbilityFactory`添加到提供者中,并在`CaslModule`模块中导出。 ```TypeScript import { Module } from '@nestjs/common'; import { CaslAbilityFactory } from './casl-ability.factory'; @Module({ providers: [CaslAbilityFactory], exports: [CaslAbilityFactory], }) export class CaslModule {} ``` 现在,只要将`CaslModule`引入对象的上下文中,就可以将`CaslAbilityFactory`注入到任何标准类中。 ```TypeScript constructor(private caslAbilityFactory: CaslAbilityFactory) {} ``` 在类中使用如下: ```TypeScript const ability = this.caslAbilityFactory.createForUser(user); if (ability.can(Action.Read, 'all')) { // "user" has read access to everything } ``` > `Ability`类更多细节参见[CASL 官方文档](https://casl.js.org/v4/en/guide/intro)。 例如,一个非管理员用户,应该可以阅读文章,但不允许创建一篇新文章或者删除一篇已有文章。 ```TypeScript const user = new User(); user.isAdmin = false; const ability = this.caslAbilityFactory.createForUser(user); ability.can(Action.Read, Article); // true ability.can(Action.Delete, Article); // false ability.can(Action.Create, Article); // false ``` > 虽然`Ability`和`AlbilityBuilder`类都提供`can`和`cannot`方法,但其目的并不一样,接受的参数也略有不同。 依照我们的需求,一个用户应该能更新自己的文章。 ```TypeScript const user = new User(); user.id = 1; const article = new Article(); article.authorId = user.id; const ability = this.caslAbilityFactory.createForUser(user); ability.can(Action.Update, article); // true article.authorId = 2; ability.can(Action.Update, article); // false ``` 如你所见,`Ability`实例允许我们通过一种可读的方式检查许可。`AbilityBuilder`采用类似的方式允许我们定义许可(并定义不同条件)。查看官方文档了解更多示例。 ### 进阶:通过策略守卫的实现 本节我们说明如何声明一个更复杂的守卫,用来配置在方法层面(也可以配置在类层面)检查用户是否满足权限策略。在本例中,将使用CASL包进行说明,但它并不是必须的。同样,我们将使用前节创建的`CaslAbilityFactory`提供者。 首先更新我们的需求。目的是提供一个机制来检查每个路径处理程序的特定权限。我们将同时支持对象和方法(分别针对简易检查和面向函数式编程的目的)。 从定义接口和策略处理程序开始。 ```TypeScript import { AppAbility } from '../casl/casl-ability.factory'; interface IPolicyHandler { handle(ability: AppAbility): boolean; } type PolicyHandlerCallback = (ability: AppAbility) => boolean; export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback; ``` 如上所述,我们提供了两个可能的定义策略处理程序的方式,一个对象(实现了`IPolicyHandle`接口的类的实例)和一个函数(满足`PolicyHandlerCallback`类型)。 接下来创建一个`@CheckPolicies()`装饰器,该装饰器允许配置访问特定资源需要哪些权限。 ```TypeScript export const CHECK_POLICIES_KEY = 'check_policy'; export const CheckPolicies = (...handlers: PolicyHandler[]) => SetMetadata(CHECK_POLICIES_KEY, handlers); ``` 现在创建一个`PoliciesGuard`,它将解析并执行所有和路径相关的策略程序。 ```TypeScript @Injectable() export class PoliciesGuard implements CanActivate { constructor( private reflector: Reflector, private caslAbilityFactory: CaslAbilityFactory, ) {} async canActivate(context: ExecutionContext): Promise<boolean> { const policyHandlers = this.reflector.get<PolicyHandler[]>( CHECK_POLICIES_KEY, context.getHandler(), ) || []; const { user } = context.switchToHttp().getRequest(); const ability = this.caslAbilityFactory.createForUser(user); return policyHandlers.every((handler) => this.execPolicyHandler(handler, ability), ); } private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) { if (typeof handler === 'function') { return handler(ability); } return handler.handle(ability); } } ``` > 在本例中,我们假设`request.user`包含了用户实例。在你的应用中,可能将其与你自定义的认证守卫关联。参见认证章节。 我们分析一下这个例子。`policyHandlers`是一个通过`@CheckPolicies()`装饰器传递给方法的数组,接下来,我们用`CaslAbilityFactory#create`方法创建`Ability`对象,允许我们确定一个用户是否拥有足够的许可去执行特定行为。我们将这个对象传递给一个可能是函数或者实现了`IPolicyHandler`类的实例的策略处理程序,暴露出`handle()`方法并返回一个布尔量。最后,我们使用`Array#every`方法来确保所有处理程序返回`true`。 为了测试这个守卫,我们绑定任意路径处理程序,并且注册一个行内的策略处理程序(函数实现),如下: ```TypeScript @Get() @UseGuards(PoliciesGuard) @CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Article)) findAll() { return this.articlesService.findAll(); } ``` 我们也可以定义一个实现了`IPolicyHandler`的类来代替函数。 ```TypeScript export class ReadArticlePolicyHandler implements IPolicyHandler { handle(ability: AppAbility) { return ability.can(Action.Read, Article); } } ``` 并这样使用。 ```TypeScript @Get() @UseGuards(PoliciesGuard) @CheckPolicies(new ReadArticlePolicyHandler()) findAll() { return this.articlesService.findAll(); } ``` > 由于我们必须使用 `new`关键词来实例化一个策略处理函数,`CreateArticlePolicyHandler`类不能使用注入依赖。这在`ModuleRef#get`方法中强调过,参见[这里](8/fundamentals.md#依赖注入))。基本上,要替代通过`@CheckPolicies()`装饰器注册函数和实例,你需要允许传递一个`Type<IPolicyHandler>`,然后在守卫中使用一个类型引用(`moduleRef.get(YOUR_HANDLER_TYPE`)获取实例,或者使用`ModuleRef#create`方法进行动态实例化。