1.1.3 • Published 2 years ago

@lcsf/acl v1.1.3

Weekly downloads
-
License
MIT
Repository
-
Last release
2 years ago

快速开始

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

NameDescription
[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

NameTypeSummaryDefault
[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, oneOfallOf 表示必须满足所有角色或权限点数组算有效 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]未授权时显示booleanfalse

类型说明

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);
}
1.1.1

3 years ago

1.1.3

2 years ago

1.1.2

3 years ago

1.1.0

3 years ago

1.0.5

3 years ago

1.0.4

3 years ago

1.0.3

3 years ago

1.0.2

3 years ago

1.0.0

3 years ago

1.0.7

3 years ago

1.0.6

3 years ago