## 授权(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`方法进行动态实例化。
- 介绍
- 概述
- 第一步
- 控制器
- 提供者
- 模块
- 中间件
- 异常过滤器
- 管道
- 守卫
- 拦截器
- 自定义装饰器
- 基础知识
- 自定义提供者
- 异步提供者
- 动态模块
- 注入作用域
- 循环依赖
- 模块参考
- 懒加载模块
- 应用上下文
- 生命周期事件
- 跨平台
- 测试
- 技术
- 数据库
- Mongo
- 配置
- 验证
- 缓存
- 序列化
- 版本控制
- 定时任务
- 队列
- 日志
- Cookies
- 事件
- 压缩
- 文件上传
- 流式处理文件
- HTTP模块
- Session(会话)
- MVC
- 性能(Fastify)
- 服务器端事件发送
- 安全
- 认证(Authentication)
- 授权(Authorization)
- 加密和散列
- Helmet
- CORS(跨域请求)
- CSRF保护
- 限速
- GraphQL
- 快速开始
- 解析器(resolvers)
- 变更(Mutations)
- 订阅(Subscriptions)
- 标量(Scalars)
- 指令(directives)
- 接口(Interfaces)
- 联合类型
- 枚举(Enums)
- 字段中间件
- 映射类型
- 插件
- 复杂性
- 扩展
- CLI插件
- 生成SDL
- 其他功能
- 联合服务
- 迁移指南
- Websocket
- 网关
- 异常过滤器
- 管道
- 守卫
- 拦截器
- 适配器
- 微服务
- 概述
- Redis
- MQTT
- NATS
- RabbitMQ
- Kafka
- gRPC
- 自定义传输器
- 异常过滤器
- 管道
- 守卫
- 拦截器
- 独立应用
- Cli
- 概述
- 工作空间
- 库
- 用法
- 脚本
- Openapi
- 介绍
- 类型和参数
- 操作
- 安全
- 映射类型
- 装饰器
- CLI插件
- 其他特性
- 迁移指南
- 秘籍
- CRUD 生成器
- 热重载
- MikroORM
- TypeORM
- Mongoose
- 序列化
- 路由模块
- Swagger
- 健康检查
- CQRS
- 文档
- Prisma
- 静态服务
- Nest Commander
- 问答
- Serverless
- HTTP 适配器
- 全局路由前缀
- 混合应用
- HTTPS 和多服务器
- 请求生命周期
- 常见错误
- 实例
- 迁移指南
- 发现
- 谁在使用Nest?