• 基于三段式命令及筛选器的rbac权限控制方案


    引用自 摸鱼wiki,点击可查看原文

    1. 权限命令的表示

    1.1 权限命令

    采用三段式设计,将命令划分为类型、操作、属性/对象三个层级。相较于采用常量声明,这样的写法可以支持使用通配符进行设置,减少调用时的代码量。

    // 常量
    File_Add
    File_Delete
    File_Switch_Page
    File_Switch_Step
    
    // 三段式
    File::Add
    File::Delete
    File::Switch::Page
    File::Switch::Step
    
    // 通配符(常量无法做到)
    File::Switch::* = [
      File::Switch::Page,
      File::Switch::Step
    ]
    File::*::* = [
      File::Add,
      File::Delete,
      File::Switch::Page,
      File::Switch::Step
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    1.2 权限作用域筛选器

    形如 属性名/满足条件的属性值 结构,表示允许通过当前权限的必要属性以及对应的属性值。

    // 属性名/满足条件的值
    creator/user1  仅允许具有creator属性,且值为user1的对象进行操作
    operator/user2  仅允许操作者为user2进行操作
    color/red,black 仅允许具有color属性,且值为red或者black##的对象进行操作
    
    • 1
    • 2
    • 3
    • 4

    2. 权限校验模块

    该模块主要分为五大功能:权限命令解析,权限作用域筛选器解析,添加权限命令,移除权限命令以及权限校验。

    2.1 权限命令解析

    权限命令是三段式命令,中间采用 :: 隔开。因此只要按照规范进行字符串分拆即可。

    /**
     * 权限命令解析器
     * @param permissionName 权限命令
     */
    function _cmdParser(permissionName: string) {
      const [type, action = '*', attr = '*'] = permissionName.split('::');
      return [type, action, attr];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    由于允许用户传入通配符格式的权限命令,为了方便计算,内部需要把通配符命令转换成具名命令,方便内部进行匹配

    /**
     * 预定义的权限命令表
     */
    PermissionHandler.PermissionMap: { [type: string]: { [action: string]: { [attr: string]: boolean } } } = {};
    
    /**
     * 判断命令是否合法
     * @param cmd 解析后的命令
     */
    function _valid(cmd: string[]) {
      const [type, action, attr] = cmd;
      if (type === '*' || action == '*') {
        return true;
      }
      return !!(
        PermissionHandler.PermissionMap[type] &&
        PermissionHandler.PermissionMap[type][action] &&
        PermissionHandler.PermissionMap[type][action][attr]
      );
    }
    
    /**
     * 获取显式权限命令(Type和Action不能为*)
     * @param cmd 权限命令
     * @returns
     */
    function _extractExplicitPermission(cmd: readonly string[]) {
      let typeKeys: string[] = [];
      let actionKeys: string[][] = [];
      let explicitPermission: string[] = [];
      if (cmd[0] === '*') {
        typeKeys = Object.keys(PermissionHandler.PermissionMap);
      } else {
        typeKeys = [cmd[0]];
      }
    
      if (cmd[1] === '*') {
        typeKeys.forEach((typeKey) => {
          actionKeys.push(Object.keys(PermissionHandler.PermissionMap[typeKey]));
        });
      } else {
        typeKeys.forEach((typeKey) => {
          actionKeys.push([cmd[1]]);
        });
      }
    
      for (let i = 0; i < typeKeys.length; i++) {
        for (let j = 0; j < actionKeys[i].length; j++) {
          const newCmd = [typeKeys[i], actionKeys[i][j], cmd[2]];
          if (_valid(newCmd)) {
            explicitPermission.push(newCmd.join('::'));
          }
        }
      }
      return explicitPermission;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    2.2 权限作用域筛选器解析

    权限作用域筛选器的格式形如 x/y,z,而且输入可能出现 [x/y, x/z],因此需要把相同属性名进行聚合,并且保证满足条件的属性值不重复。为了提高查找时的性能,这里采用了 Map + Set 结构进行存储,保证在查找时满足 O(1) 时间复杂度。

    /**
     * 筛选器解析器
     * @param filters 筛选器列表(形如:creator/xxx)
     */
    function _filterParser(filters: readonly string[]): Map<string, Set<string>> {
      const filterMap = new Map<string, Set<string>>();
      for (const filter of filters) {
        const [attr, conditions] = filter.split('/');
        const nSet = new Set(conditions.split(','));
        const oSet = filterMap.get(attr);
        if (oSet) {
          filterMap.set(attr, new Set([...oSet, ...nSet]));
        } else {
          filterMap.set(attr, nSet);
        }
      }
      return filterMap;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2.3 添加权限命令

    这一模块用于批量添加权限命令及其对应的筛选器。主要工作流程如下:

    1. 解析作用域筛选器
    2. 解析命令,并提取具名权限命令
    3. 具名权限命令设置作用域筛选器
    /**
     * 权限拦截器存储结构
     */
    const interceptorMap = new Map<string, Map<string, Set<string>>>();
    
    /**
     * 添加权限拦截器
     * @param permissions 权限名称
     * @param filter 筛选器
     */
    function setInterceptor(permissions: string[], filter: readonly string[]) {
      let explicitCmds: string[] = [];
      // 解析作用域筛选器
      const filters = _filterParser(filter);
      // 解析命令,并提取具名权限命令
      permissions.forEach((permissionName: string) => {
        const cmd = _cmdParser(permissionName);
        if (!_valid(cmd)) {
          return;
        }
        const explicitCmd = _extractExplicitPermission(cmd);
        explicitCmds = explicitCmds.concat(explicitCmd);
      });
      // 具名权限命令设置作用域筛选器
      explicitCmds.forEach((dCmd) => {
        const newFilters = filters;
        if (interceptorMap.has(dCmd)) {
          const old = interceptorMap.get(dCmd);
          old?.forEach((val, key) => {
            if (!newFilters.has(key)) {
              newFilters.set(key, val);
            }
          })
        }
        interceptorMap.set(dCmd, newFilters);
      });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    2.4 移除权限命令

    这一模块用于批量删除权限命令及其对应的筛选器。主要工作流程如下:

    1. 解析命令,并提取具名权限命令
    2. 移除对应权限命令
    /**
     * 移除权限拦截器
     * @param permissions 权限名称
     */
    removeInterceptor(permissions: string[]) {
      let explicitCmds: string[] = [];
      permissions.forEach((permissionName: string) => {
        const cmd = _cmdParser(permissionName);
        if (!_valid(cmd)) {
          return;
        }
        const explicitCmd = _extractExplicitPermission(cmd);
        explicitCmds = explicitCmds.concat(explicitCmd);
      });
      explicitCmds.forEach((dCmd) => {
        interceptorMap.delete(dCmd);
      });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2.5 权限校验

    这个功能是整个模块的灵魂,前面的所有功能都是为其服务的。其主要工作流程可划分为下列模块:

    2.5.1 解析命令
    const cmd = _cmdParser(permissionName);
    // 命令不合法,跳过权限校验
    if (!_valid(cmd)) {
      return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    2.5.2 获取最高权限对应的作用域筛选器

    由于权限命令是三段命令式,用户可能在不同的层级上设置了不同的权限作用域筛选器。因此,需要给各个层级定义筛选器生效的优先级。

    本方案的优先级定义是:

    三级具名命令 > 三级模糊命令 > 二级具名命令 > 二级模糊命令 > 一级具名命令 > 一级模糊命令

    // 以 File::Switch::Page 举例
    // 优先级从高到低依次为
    
    File::Switch::Page
    File::Switch::*
    File::*::*
    *::*::*
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    对应的代码实现为:

    /**
     * 获取最高权限的筛选器
     * @param cmd 权限命令
     */
    function getHighestAuthorityFilter(cmd: readonly string[]) {
      const realCmd = cmd.concat([]);
      let loop = 3;
      let cmdStr = '';
    
      while (loop) {
        cmdStr = realCmd.join('::');
        if (interceptorMap.has(cmdStr)) {
          return interceptorMap.get(cmdStr);
        }
        realCmd[--loop] = '*';
      }
    
      return undefined;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    2.5.3 权限可通过性判断

    conditions 为待判断的对象,通过遍历对象上与作用域筛选器相同的字段,检测对象上的对应值是否在筛选器的可通过条件列表中。如是,返回校验通过,否则,返回校验不通过。

    function checkHasPermission(conditions: any[], filters: Map<string, Set<string>>) {
      let hasPermission = true;
      if (filters) {
        filters.forEach((valSet, attr) => {
          // 短路运算(已评为不满足/属性值带有*)
          if (!hasPermission || valSet.has('*')) return;
          // 任意属性均需要满足条件
          if (attr === '*') {
            for (const obj of conditions) {
              for (const attrKey of Object.keys(obj)) {
                if (!valSet.has(obj[attrKey])) {
                  hasPermission = false;
                  break;
                }
              }
              if (!hasPermission) {
                break;
              }
            }
          } else {
            for (const obj of conditions) {
              if (!valSet.has(obj[attr])) {
                hasPermission = false;
                break;
              }
            }
          }
        });
      }
      return hasPermission;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    2.5.4 模块代码总览
    /**
     * 判断是否具有操作权限
     * @param permissionName 权限名称
     * @param conditions 受影响的对象
     */
    function checkPermission(permissionCmd: string, conditions: readonly { [key: string]: any }[]): boolean {
      // 解析命令
      const cmd = _cmdParser(permissionCmd);
      if (!_valid(cmd)) {
        return true;
      }
      // 获取最高权限对应的作用域筛选器
      const filters = getHighestAuthorityFilter(cmd);
      // 权限可通过性判断
      return checkHasPermission(conditions, filters);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    3. 使用案例

    // 禁用全部操作权限
    
    setInterceptor(['*::*::*'], ['operator/']);
    // 'operator/' 表示对于操作者(operator)这个条件,符合的值是空的,意味着没有操作者能够被允许通过这个权限校验
    
    checkPermission('File::Switch::Page', [{ operator: 'xxx' }])
    // false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    // 启用全部操作权限
    
    setInterceptor(['*::*::*'], ['operator/*']);
    // 'operator/*' 表示对于操作者(operator)这个条件,符合的值是任意的,意味着所有操作者能够被允许通过这个权限校验
    
    checkPermission('File::Switch::Page', [{ operator: 'xxx' }])
    // true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  • 相关阅读:
    最大似然估计和最小二乘法 含代码
    java设计模式【单例模式】
    Java开发常见英语词汇汇总
    BigInteger和BigDecimal的使用
    C高级 shell指令分支和循环
    pyecharts画图结果存为图片
    Spring Boot整合Swagger报错:“this.condition“ is null
    漫谈:C语言 C++ 函数返回值究竟是什么
    【数据结构】循环队列的实现
    静态代理和动态代理笔记
  • 原文地址:https://blog.csdn.net/think_A_lot/article/details/127795198