0x0 前言
系統授權指的是登錄用戶執行操作過程,比如管理員可以對系統進行用戶操作、網站帖子管理操作,非管理員可以進行授權閱讀帖子等操作,所以實現需要對系統的授權需要身份驗證機制,下面來實現最基本的基于角色的訪問控制系統。
0x1 RBAC 實現
基于角色的訪問控制(RBAC)是圍繞角色的特權和定義的策略無關的訪問控制機制,首先創建個代表系統角色枚舉信息 role.enum.ts:
1
2
3
4
|
export enum Role { User = 'user' , Admin = 'admin' } |
如果是更復雜的系統,推薦角色信息存儲到數據庫更好管理。
然后創建裝飾器和使用 @Roles() 來運行指定訪問所需要的資源角色,創建roles.decorator.ts:
1
2
3
4
5
|
import { SetMetadata } from '@nestjs/common' import { Role } from './role.enum' export const ROLES_KEY = 'roles' export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles) |
上述創建一個名叫 @Roles() 的裝飾器,可以使用他來裝飾任何一個路由控制器,比如用戶創建:
1
2
3
4
5
|
@Post() @Roles(Role.Admin) create(@Body() createUserDto: CreateUserDto): Promise<UserEntity> { return this .userService.create(createUserDto) } |
最后創建一個 RolesGuard 類,它會實現將分配給當前用戶角色和當前路由控制器所需要角色進行比較,為了訪問路由角色(自定義元數據),將使用 Reflector 工具類,新建 roles.guard.ts:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common' import { Reflector } from '@nestjs/core' import { Role } from './role.enum' import { ROLES_KEY } from './roles.decorator' @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requireRoles = this .reflector.getAllAndOverride<Role[]>(ROLES_KEY, [context.getHandler(), context.getClass()]) if (!requireRoles) { return true } const { user } = context.switchToHttp().getRequest() return requireRoles.some(role => user.roles?.includes(role)) } } |
假設 request.user 包含 roles 屬性:
1
2
3
4
|
class User { // ...other properties roles: Role[] } |
然后 RolesGuard 在控制器全局注冊:
1
2
3
4
5
6
|
providers: [ { provide: APP_GUARD, useClass: RolesGuard } ] |
當某個用戶訪問超出角色范疇內的請求出現:
1
2
3
4
5
|
{ "statusCode" : 403, "message" : "Forbidden resource" , "error" : "Forbidden" } |
0x2 基于聲明的授權
創建身份后,系統可以給身份分配一個或者多個聲明權限,表示告訴當前用戶可以做什么,而不是當前用戶是什么,在 Nest 系統里實現基于聲明授權,步驟和上面 RBAC 差不多,但有個區別是,需要比較權限,而不是判斷特定角色,每個用戶分配一組權限,比如定一個 @RequirePermissions() 裝飾器,然后訪問所需的權限屬性:
1
2
3
4
5
|
@Post() @RequirePermissions(Permission.CREATE_USER) create(@Body() createUserDto: CreateUserDto): Promise<UserEntity> { return this .userService.create(createUserDto) } |
Permission 表示類似 PRAC 中的 Role 枚舉,包含其中系統可訪問的權限組:
1
2
3
4
|
export enum Role { CREATE_USER = [ 'add' , 'read' , 'update' , 'delete' ], READ_USER = [ 'read' ] } |
0x3 集成 CASL
CASL 是一個同構授權庫,可以限制客戶端訪問的路由控制器資源,安裝依賴:
1
|
yarn add @casl /ability |
下面使用最簡單的例子來實現 CASL 的機制,創建 User 和 Article 倆個實體類:
1
2
3
4
|
class User { id: number isAdmin: boolean } |
User 實體類倆個屬性,分別是用戶編號和是否具有管理員權限。
1
2
3
4
5
|
class Article { id: number isPublished: boolean authorId: string } |
Article 實體類有三個屬性,分別是文章編號和文章狀態(是否已經發布)以及撰寫文章的作者編號。
根據上面倆個最簡單的例子組成最簡單的功能:
- 具有管理員權限的用戶可以管理所有實體(創建、讀取、更新和刪除)
- 用戶對所有內容只有只讀訪問權限
- 用戶可以更新自己撰寫的文章 authorId === userId
- 已發布的文章無法刪除 article.isPublished === true
針對上面功能可以創建 Action 枚舉,來表示用戶對實體的操作:
1
2
3
4
5
6
7
|
export enum Action { Manage = 'manage' , Create = 'create' , Read = 'read' , Update = 'update' , Delete = 'delete' , } |
manage 是 CASL 中的特殊關鍵字,表示可以進行任何操作。
實現功能需要二次封裝 CASL 庫,執行 nest-cli 進行創建需要業務:
1
2
|
nest g module casl nest g class casl /casl-ability .factory |
定義 CaslAbilityFactory 的 createForUser() 方法,來未用戶創建對象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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' ) // 允許任何讀寫操作 } else { can(Action.Read, 'all' ) // 只讀操作 } can(Action.Update, Article, { authorId: user.id }) cannot(Action.Delete, Article, { isPublished: true }) return build({ // 詳細:https://casl.js.org/v5/en/guide/subject-type-detection#use-classes-as-subject-types detectSubjectType: item => item.constructor as ExtractSubjectType<Subjects> }) } } |
然后在 CaslModule 引入:
1
2
3
4
5
6
7
8
|
import { Module } from '@nestjs/common' import { CaslAbilityFactory } from './casl-ability.factory' @Module({ providers: [CaslAbilityFactory], exports: [CaslAbilityFactory] }) export class CaslModule {} |
然后在任何業務引入 CaslModule 然后在構造函數注入就可以使用了:
1
2
3
4
5
6
|
constructor(private caslAbilityFactory: CaslAbilityFactory) {} const ability = this .caslAbilityFactory.createForUser(user) if (ability.can(Action.Read, 'all' )) { // "user" 對所有內容可以讀寫 } |
如果當前用戶是普通權限非管理員用戶,可以閱讀文章,但不能創建新的文章和刪除現有文章:
1
2
3
4
5
6
7
|
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 |
這樣顯然有問題,當前用戶如果是文章的作者,應該可以對此進行操作:
1
2
3
4
5
6
7
8
9
10
11
|
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 |
0x4 PoliceiesGuard
上述簡單的實現,但在復雜的系統中還是不滿足更復雜的需求,所以配合上一篇的身份驗證文章來進行擴展類級別授權策略模式,在原有的 CaslAbilityFactory 類進行擴展:
1
2
3
4
5
6
7
8
9
|
import { AppAbility } from '../casl/casl-ability.factory' interface IPolicyHandler { handle(ability: AppAbility): boolean } type PolicyHandlerCallback = (ability: AppAbility) => boolean export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback |
提供支持對象和函數對每個路由控制器進行策略檢查:IPolicyHandler 和 PolicyHandlerCallback。
然后創建一個 @CheckPolicies() 裝飾器來運行指定訪問特定資源策略:
1
2
|
export const CHECK_POLICIES_KEY = 'check_policy' export const CheckPolicies = (...handlers: PolicyHandler[]) => SetMetadata(CHECK_POLICIES_KEY, handlers) |
創建 PoliciesGuard 類來提取并且執行綁定路由控制器所有策略:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
@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() 分配,使用 aslAbilityFactory#create 構造 Ability 對象方法,來驗證用戶是否具有足夠的權限來執行特定的操作,然后將此對象傳遞給策略處理方法,該方法可以實現函數或者是類的實例 IPolicyHandler,并且公開 handle() 方法返回布爾值。
1
2
3
4
5
6
|
@Get() @UseGuards(PoliciesGuard) @CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Article)) findAll() { return this .articlesService.findAll() } |
同樣可以定義 IPolicyHandler 接口類:
1
2
3
4
5
|
export class ReadArticlePolicyHandler implements IPolicyHandler { handle(ability: AppAbility) { return ability.can(Action.Read, Article) } } |
使用如下:
1
2
3
4
5
6
|
@Get() @UseGuards(PoliciesGuard) @CheckPolicies( new ReadArticlePolicyHandler()) findAll() { return this .articlesService.findAll() } |
到此這篇關于Nest.js 授權驗證的方法示例的文章就介紹到這了,更多相關Nest.js 授權驗證內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://iiong.com/nest-js-authorization-verification/