@lcsf/acl
Version:
权限控制模块
457 lines (362 loc) • 11.6 kB
Markdown
## 快速开始
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>
```
按钮必须拥有 5 或 1 角色显示。
```html
<button [lcAcl]="['5', '1']"></button> <button *lcAclIf="['5', '1']"></button>
```
按钮必须拥有 5 和 1 角色显示。
```html
<button [lcAcl]="{ role: ['5', '1'], mode: 'allOf' }"></button>
<button *lcAclIf="{ role: ['5', '1'], mode: 'allOf' }"></button>
```
按钮必须拥有 角色是 3-0 或 3-2 或 5 ( `3-0: 3为角色,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);
}
```