@lcsf/acl v1.1.3
快速开始
ACL 全称叫访问控制列表(Access Control List),是一种非常简单的基于角色权限控制方式
使用
Install `lc-acl:
npm add lc-acl
Import LcAclModule
module:
import { LcAclModule } from 'lc-acl';
@NgModule({
imports: [
LcAclModule.forRoot()
]
})
export class AppModule { }
// 子模块
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
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 角色显示。
<button [lcAcl]="'5'"></button> <button *lcAclIf="'5'"></button>
按钮必须拥有 5 或 1 角色显示。
<button [lcAcl]="['5', '1']"></button> <button *lcAclIf="['5', '1']"></button>
按钮必须拥有 5 和 1 角色显示。
<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}
<button [lcAcl]="['3-0', '3-2','5']"></button>
<button *lcAclIf="['3-0', '3-2', '5']"></button>
当拥有 5 角色显示文本框,未授权显示文本。
<input nz-input *lcAclIf="'5'; else unauthorized" /> <ng-container #unauthorized>{{user}}</ng-container>
使用 except
反向控制,当未拥有 5 角色时显示。
<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为真 显示。
<button [lcAcl]="{ role: ['5'], extraOne: true }"></button>
<button *lcAclIf="{ role: ['5'], extraOne: true }"></button>
按钮拥有角色 并且 extraAll 为真 显示。
<button [lcAcl]="{ role: ['5'], extraAll: true}"></button>
<button *lcAclIf="{ role: ['5'], extraAll: true }"></button>
权限点
按钮必须拥有 退款 权限点显示。
<button [lcAcl]="refund::trade::write"></button>
按钮必须拥有 退款或导出 权限点显示。
<button [lcAcl]="['refund::trade::write', 'trade::export::write']"></button>
acl 指令为了能所传递的值是角色还是权限点,所以带有 ::
表示权限点,否则表示角色
使用 mode: 'allOf'
表示必须同时拥有。
oneOf
表示只须满足角色或权限点数组中的一项算有效(默认)allOf
表示必须满足所有角色或权限点数组算有效
按钮必须拥有 退款和导出 权限点时显示。
<button [lcAcl]="{ permissions: ['refund::trade::write', 'trade::export::write'], mode: 'allOf' }"></button>
同理在js层逻辑判断的时候也可以直接使用LcAclService来做判断;
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 |
类型说明
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的情况,提供一个方法,该方法会查找当前模块可以访问的第一个页面路由,针对大菜单级别路由,也提供一个查找当前大菜单可以访问的第一个模块
// 一段路由配置
{
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页面,如果不配置就会出现白屏的情况
解决方案:
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',
}]
// 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可以访问的第一个页面路由
// 源码说明
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);
}