## 授權(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?