Nestjs RBAC 許可權控制管理實踐 (二)
上回分析了 node-casbin 模組, Casbin 主要是提供了多種的許可權控制策略, 支援 ACL, RBAC, ABAC, RESTful 以及複雜的拒絕覆蓋, 許可權優先等級等。 不得不說 Casbin 功能強大,不過對於我的專案需求,直接用起來,看似又比較複雜了。 因為我這個專案主要是用RESTful API, 所以借鑑了 accesscontrol 和 casbin 的 RESTful 的控制模式簡化成我要的版本,具體如下:
1. @RolesGuard 與 @Roles 的關係
上回我們看官網的方式是,使用 @RolesGuard 和 @Roles 組合的方式, 使用 @RolesGuard 去守護, @Roles 則去標記哪個角色可以通過。
我們回顧下程式碼 @RolesGuard 搭檔 @Roles
@Controller('cats') @UseGuards(RolesGuard) @UseInterceptors(LoggingInterceptor, TransformInterceptor) export class CatsController { constructor(private readonly catsService: CatsService) {} @Post() @Roles('admin') async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); } } 複製程式碼
2. AuthGuard 是如何實現的,為什麼能帶引數。
由於注意到常用的 AuthGuard('jwt') 是又引數的, 但是 RolesGuard 是沒有引數的, 也無法帶上引數, 所以特地去翻了下 AuthGuard 的原始碼.auth.guard.ts
export const AuthGuard: (type?: string) => Type<IAuthGuard> = memoize( createAuthGuard ); function createAuthGuard(type?: string): Type<CanActivate> { class MixinAuthGuard<TUser = any> implements CanActivate { ... } } 複製程式碼
3.RESTFul 的動作匹配
對比 AuthGuard 發現其是比較特殊的函式方式返回一個 CanActivate 的實現類, 這個實現比較複雜,同時也失去了依賴注入的能力。 並且由於專案需求是由界去配置許可權和 API 的關聯關係的,所以這裡並不能直接用角色去關聯API。 還有另外一個原因是 RESTFul 的 API 可以採用約定的規則來匹配許可權,所以並不必要每個 API 去打標記, 匹配形式為 :
{ GET: 'read', // 讀取 POST: 'create', // 建立 PUT: 'update',// 更新 DELETE: 'delete', // 刪除 } 複製程式碼
4.@RolesGuard 如何得到控制器的註解資訊(這個花了點時間搞出來)
經過上面的處理, 我們在控制器上可以解放出來了,我們只要一個 @RolesGuard, 並不要 @Roles 來配合了。
但由於 @RolesGuard 無法傳遞控制器引數, 所以我們只能另尋辦法了, 想到 @RolesGuard 能獲取到 @Roles 裡註解引數, 我們是不是能從 @Controller 裡獲得引數呢? 這樣我們就能定位當前的請求資源了,於是有了下面的程式碼:最終程式碼
async canActivate(context: ExecutionContext): Promise<boolean> { const roles = this.reflector.get<string[]>( 'roles', context.getHandler(), ); // 這裡我們能從 context.getHandler() 裡得到 roles; /** 那麼我們也就能從context.getClass() 裡得到 @Controller 裡的註解引數, 當然,我們也能從 request.url 裡分析得到,但是控制器的註解有時候可能寫的複雜 如 abc/efg 我們就不知道怎麼截斷了。 **/ const ctrl = this.reflector.get<string>('path', context.getClass()); ... } 複製程式碼
5. 關聯 API 到選單。
我寫了一個 API 的描述檔案
const actions = { create: '建立', read: '讀取', update: '更新', delete: '刪除', }; export interface GrantNode { name: string; actions: { [k: string]: string; create?: string; read?: string; update?: string; delete?: string; }; } export const grants: { [key: string]: GrantNode; } = { dict: { name: '字典', actions, }, group: { name: '使用者組', actions, }, log: { name: '日誌', actions, }, menu: { name: '選單', actions, }, notice: { name: '通知', actions, }, role: { name: '角色', actions, }, setting: { name: '設定', actions, }, user: { name: '使用者', actions, }, }; 複製程式碼