UNPKG

@lcsf/acl

Version:

权限控制模块

457 lines (362 loc) 11.6 kB
## 快速开始 ACL 全称叫访问控制列表(Access Control List),是一种非常简单的基于角色权限控制方式 ## 使用 Install `lc-acl: ```bash npm add lc-acl ``` Import `LcAclModule` module: ```typescript import { LcAclModule } from 'lc-acl'; @NgModule({ imports: [ LcAclModule.forRoot() ] }) export class AppModule { } ``` ```typescript // 子模块 import { LcAclModule } from 'lc-acl'; @NgModule({ imports: [ LcAclModule ] }) export class XXXModule { } ``` ### ACLService | Name | Description | |------|-------------| | `[change]` | 监听ACL变更通知 | | `[data]` | 获取所有ACL数据 | | `setFull(val: boolean)` | 标识当前用户为全量,即不受限 | | `set(value: ACLType)` | 设置当前用户角色或权限能力(会先清除所有) | | `setRole(roles: string[])` | 设置当前用户角色(会先清除所有) | | `add(value: ACLType)` | 为当前用户增加角色或权限能力 | | `attachRole(roles: string[])` | 为当前用户附加角色 | | `removeRole(roles: string[])` | 为当前用户移除角色 | | `can(roleOrPermission: ACLCanType)` | 当前用户是否有对应角色|权限 | | `canAuthUrl(url: string)` | 当前用户是否有打开当前页面的权限 | | `getUrlModeId(url: string)` | 获取当前页面url所属的菜单模块id | | `getAuthPaths(menus: AuthModelList[])` | 根据传入的菜单数据获取当前用户可访问的路由表 | ### LcACLCanType ```ts type LcACLCanType = string | string[] | LcACLType; ``` ### ACLType | Name | Type | Summary | Default | |------|------|---------|---------| | `[role]` | `string[]` | 角色 | - | | `[type]` | `string[]` | 用户类型 | - | | `[permissionGroups]` | `string[]` | 用户的权限列表组,单个权限是由 :: 连接,例如: trade::refund::read | - | | `[permissions]` | `string[]` | 待校验的权限列表,单个权限是由 :: 连接,例如: trade::refund::read | - | | `[menus]` | `AuthModelList[]` | 用户的所有菜单数据 | - | | `[authPaths]` | `Map<string, string>` | 整理菜单数据得到的授权路由表,Map接口,url为key,model_id为value | - | | `[authPath]` | `string` | 待验证的path | - | | `[mode]` | `allOf, oneOf` | `allOf` 表示必须满足所有角色或权限点数组算有效 `oneOf` 表示只须满足角色或权限点数组中的一项算有效 | `oneOf` | | `[except]` | `boolean` | 是否取反,即结果为 true 时表示未授权 | `false` | ## 粒度控制 ## 写在前面 很多时候需要对某个按钮进行权限控制,`lc-acl` 提供一个 `lcAcl` 指令,可以利用角色或权限点对某个按钮、表格、列表等元素进行权限控制。 ## 原理 `[lcAcl]` 默认会在目标元素上增加一个 `lcAcl__hide` 样式,利用 `display: none` 来隐藏未授权元素,它是一个简单、又高效的方式。 以此相对应的 `*lcAclIf` 是一个结构型指令,它类似 `ngIf` 在未授权时会不渲染该元素。 ## 示例 ### 角色 按钮必须拥有 5 角色显示。 ```html <button [lcAcl]="'5'"></button> <button *lcAclIf="'5'"></button> ``` 按钮必须拥有 51 角色显示。 ```html <button [lcAcl]="['5', '1']"></button> <button *lcAclIf="['5', '1']"></button> ``` 按钮必须拥有 51 角色显示。 ```html <button [lcAcl]="{ role: ['5', '1'], mode: 'allOf' }"></button> <button *lcAclIf="{ role: ['5', '1'], mode: 'allOf' }"></button> ``` 按钮必须拥有 角色是 3-03-25 ( `3-03为角色,0为用户类型`) > 注意用户类型判断必须要用对象的形式描述,因为跟角色一样都是数字,没办法区分 {type: [0]} ```html <button [lcAcl]="['3-0', '3-2','5']"></button> <button *lcAclIf="['3-0', '3-2', '5']"></button> ``` 当拥有 5 角色显示文本框,未授权显示文本。 ```html <input nz-input *lcAclIf="'5'; else unauthorized" /> <ng-container #unauthorized>{{user}}</ng-container> ``` 使用 `except` 反向控制,当未拥有 5 角色时显示。 ```html <ng-container [lcAcl]="{ role: ['5'] , except: true}" > <input nz-input /> </ng-container> <ng-container *lcAclIf="{ role: ['5'] , except: true}" > <input nz-input /> </ng-container> ``` 用户自定义额外字段 `extraOne` 按钮拥有角色 或 extraOne为真 显示。 ```html <button [lcAcl]="{ role: ['5'], extraOne: true }"></button> <button *lcAclIf="{ role: ['5'], extraOne: true }"></button> ``` 按钮拥有角色 并且 extraAll 为真 显示。 ```html <button [lcAcl]="{ role: ['5'], extraAll: true}"></button> <button *lcAclIf="{ role: ['5'], extraAll: true }"></button> ``` ### 权限点 按钮必须拥有 退款 权限点显示。 ```html <button [lcAcl]="refund::trade::write"></button> ``` 按钮必须拥有 退款或导出 权限点显示。 ```html <button [lcAcl]="['refund::trade::write', 'trade::export::write']"></button> ``` acl 指令为了能所传递的值是角色还是权限点,所以带有 `::` 表示权限点,否则表示角色 使用 `mode: 'allOf'` 表示必须同时拥有。 - `oneOf` 表示只须满足角色或权限点数组中的一项算有效(默认) - `allOf` 表示必须满足所有角色或权限点数组算有效 按钮必须拥有 退款和导出 权限点时显示。 ```html <button [lcAcl]="{ permissions: ['refund::trade::write', 'trade::export::write'], mode: 'allOf' }"></button> ``` 同理在js层逻辑判断的时候也可以直接使用LcAclService来做判断; ```typescript xxx.component.ts import { LcAClService } from 'lc-acl'; ··· constructor(private lcAClService: LcAClService) { } ··· // 某个业务逻辑 // 判断某个请求必须是 manager 才能发出 if (this.lcAClService.can(['manager'])) { // your code } ``` ## API ### *lcAclIf 参数 | 说明 | 类型 | 默认值 ----------|----------------|----------|------- `[lcAclIf]` | `can` 方法参数体 | `ACLCanType` | - `[lcAclIfThen]` | 已授权时显示模板 | `TemplateRef<void> | null` | - `[lcAclIfElse]` | 未授权时显示模板 | `TemplateRef<void> | null` | - `[except]` | 未授权时显示 | `boolean` | `false` ## 类型说明 ```typescript import type { NzSafeAny } from 'ng-zorro-antd/core/types'; export interface LcACLType { /** * 角色 */ role?: string[]; /** * 角色 */ type?: string[]; /** * 权限组 */ permissionGroups?: string[]; /** * 权限组拼接字符组 */ permissions?: string[]; /** * 设置的可以访问的路由菜单 */ menus?: AuthModelList[]; /** * 授权可访问的菜单列表 */ authPaths?: Map<string, string>; /** * 待验证的path */ authPath?: string; /** * Validated against, default: `oneOf` * - `allOf` the value validates against all the roles or abilities * - `oneOf` the value validates against exactly one of the roles or abilities */ mode?: 'allOf' | 'oneOf'; /** * 是否取反,即结果为 `true` 时表示未授权 */ except?: boolean; [key: string]: NzSafeAny; } export type LcACLCanType = string | string[] | LcACLType; export interface LcACLConfig { /** * Router URL when guard fail, default: `/auth/403` */ guard_url?: string; } /** * 授权的菜单列表单个 */ export interface AuthPathItem { icon: string; id: number; level: number; menu_name: string; parent_id: number; url: string; children: null | AuthPathItem[]; } /** * 菜单模块列表组 */ export interface AuthModelList { menu_list: AuthPathItem[]; model_name: string; model_id: string; url: string; } ``` ## 工具方法 ### 1. 解决点击最上层面包屑的时候出现白屏或403的情况,提供一个方法,该方法会查找当前模块可以访问的第一个页面路由,针对大菜单级别路由,也提供一个查找当前大菜单可以访问的第一个模块 ```JavaScript // 一段路由配置 { path: 'merchantInfo', data: { breadcrumb: '商户信息' }, children: [ { path: 'base', data: { breadcrumb: '基本信息' }, component: BaseInfoComponent, }, { path: 'statement', data: { breadcrumb: '结算信息' }, component: StatementInfoComponent, }, { path: 'rate', data: { breadcrumb: '费率信息' }, component: RateInfoComponent, }, { path: 'auth', data: { breadcrumb: '认证信息' }, component: AuthInfoComponent, }, { path: '', redirectTo: 'base', pathMatch: 'full', }, ], } ``` 当前 merchantInfo 模块默认的路由是 /merchantInfo/base, 但是当前用户如果没有/merchantInfo/base 路由权限的话就会跳转到403页面,如果不配置就会出现白屏的情况 解决方案: ```javascript import { getMenuFirstAuthModel, getModelFirstAuthPath } from '@app/package/acl'; [{ path: 'queryPay', data: { breadcrumb: '交易查询' }, children: [ { path: 'statistics', data: { breadcrumb: '交易统计' }, children: [ { path: '', component: PayStatisticsComponent }, { path: 'detail/:outTradeNo/:orderType', component: PayStatisticsDetailComponent, data: { breadcrumb: '详情' }, }, ], }, { path: 'preAuthorization', data: { breadcrumb: '预授权交易查询' }, children: [ { path: '', component: PreAuthorizationComponent }, { path: 'detail/:tradeNo', component: PreAuthorizationDetailComponent, data: { breadcrumb: '详情' }, }, ], }, { path: '', redirectTo: getModelFirstAuthPath("/pay/queryPay"), pathMatch: 'full', }, ], }, { path: '', redirectTo: getMenuFirstAuthModel(), pathMatch: 'full', }] ``` ```javascript // lc-acl.utils.ts /** * 根据当前模块路径获取当前模块可以访问的第一个路由 * @param model_path * @returns */ export function getModelFirstAuthPath(model_path: string, menu_storage_key = 'LCmenus') { const LCmenus = JSON.parse(localStorage.getItem(menu_storage_key) || '[]'); let result_path = '/auth/403'; if (LCmenus.length > 0) { LCmenus.forEach(model_item => { if (model_item.url === model_path) { if (!model_item.children || !model_item.children.length) { result_path = model_item.url; } else { result_path = model_item.children[0].url; } } }); } return result_path; } /** * 获取当前menu第一个授权的模块path * @param menu_storage_key */ export function getMenuFirstAuthModel(menu_storage_key = 'LCmenus') { const LCmenus = JSON.parse(localStorage.getItem(menu_storage_key) || '[]'); let result_path = ''; if (LCmenus.length) { result_path = LCmenus[0].url; } console.log(result_path) return result_path; } ``` ### 2. 在当前菜单树种查找第一个最深层次的菜单url 场景:用户A在pageA页面退出之后,用户B重新登录,进入PageA,但是用户B没有PageA页面的权限,需要查找到用户B可以访问的第一个页面路由 ```typescript // 源码说明 interface MixMenu { children?: MixMenu[]; menu_list?: MixMenu[]; url: string; } /** * 提供一个方法方便查找菜单树中第一个最深层次的菜单url * @param menus * @returns */ export function findFirstUrl(menus: MixMenu[]) { let firstMenu = menus[0]; let list = firstMenu.menu_list || firstMenu.children; if (!list || !list.length) { return firstMenu.url; } return findFirstUrl(list); } ```