Skip to content

天源云的权限模型

声明

一般情况下,本文档提到的资源,通常都包括应用、菜单、视图、功能、字段、接口、数据等类型的资源。 但应用是比较特殊的资源,单独在应用维护页面配置,其他资源在资源维护页面配置。

本系统的权限模型,采用ARBAC(Acuity Role-Based Access Control,acuity系统扩展的基于角色的访问控制),它是在RBAC(Role-Based Access Control,基于角色的访问控制)的基础上,做了一些改进。

上图中,深色是主数据表,浅色是关联表(rel结尾的都是关联表),def_*表在acuity_defaults库,base_*在acuity_base_{TenantId}库。

  1. 员工-角色-资源:传统的RBAC0模式,一个员工在不同的场景下可以拥有不同的角色,每个角色拥有不同的资源。
  2. 员工-部门-角色-资源:acuity系统扩展模式,一个员工可以属于多个部门,每个部门可以绑定默认的部门角色,该部门下的员工即拥有此部门的默认角色的权限。
  3. 员工-租户-应用:一个用户可以加入多个租户,每个租户可以有不同的应用

注意:

  1. 增加、修改或删除接口(API、URI)后,需要重启项目,在选择接口时,才能加载出来。
  2. 若你的接口地址发生了变更,需要重新在此资源维护页面配置新接口!!! 如:第一版配置了 /menu/save 接口,后期调整改为 /menu/saveMenu 接口,需要在此页面删除原来的 /menu/save 接口,然后重新选择 /menu/saveMenu ,并保存数据!!!
  3. 菜单权限完全由后端接口(ResourceController#myRouter)控制数据,前端无需处理!
  4. 按钮权限完全由前端的**权限指令(v-hasPermission)权限属性(auth、authMode)**控制,后端无法干预!
  5. 接口权限完全由后端acuity-gateway的拦截器(AuthenticationFilter)控制,前端无法干预!
  6. 按钮上绑定的资源编码可以是任意的资源类型(菜单、视图、功能、字段4种类型)的编码。

步骤

acuity系统权限体系分为5个步骤:①配置 - ②授权 - ③认证 - ④鉴权 - ⑤权限控制,请认真理解每一个步骤。

资源类型

acuity系统权限体系包含7种资源类型:①应用 - ②菜单 - ③视图 - ④功能 - ⑤字段 - ⑥接口 - ⑦数据,请认真理解每种资源类型。

应用、菜单、视图、功能、字段、接口、数据在资源维护页面对其进行配置时,称他们为资源。将资源授权给员工后,查询员工拥有什么资源,可以称作查询员工的权限

认证:

认证是指确认声明者的身份。即:用户通过账号、手机号、电子邮箱等账号通过登录页面登录系统。

配置:

配置是指开发者通过页面或数据库实现配置好资源的过程, 即: - 通过开发运营系统 - 应用管理 - 应用维护 页面,对系统中所有应用等进行配置的过程。 - 通过开发运营系统 - 应用管理 - 资源维护 页面,对系统中所有的菜单、视图、功能、字段、接口、数据等资源进行配置的过程。

授权:

授权是指资源所有者委派执行者,赋予执行者指定范围的资源操作权限。简单说就是 - 平台管理员给租户分配应用的过程 - 租户管理员给普通用户授予访问某某资源的权限。

鉴权:

鉴权是指对于一个声明者所声明的身份权利的真实性进行鉴别确认的过程。鉴权是一个承上启下的一个环节,上游它接受权限的输出,校验其真实性后,然后获取权限(permission),为下一步的权限控制做好准备。结合本系统来讲,就是登陆后通过 /anyone/visible/router/anyone/visible/resource 获取鉴定身份,并获取权限的过程。

参考:ResourceController#visible 和 ResourceController#myRouter

权限控制:

权限控制是指对可执行的各种操作组合配置为权限列表,然后根据执行者的权限,若其操作在权限范围内,则允许执行,否则禁止。结合本系统来讲,就是通过拦截器(后端)或指令(前端)等方式控制用户是否可以访问某个资源

  1. 应用,应用是一种特殊的资源,在"应用维护"页面配置应用数据

  2. 菜单、视图权限通过后端/anyone/visible/router接口控制。注意:该接口只返回用户拥有的权限,没权限的菜单和视图不会返回,所以但用户访问前端无权限的路由地址时,不会跳转到403页面,而是404页面。若你想实现无权限时跳转到403,确实访问了错误的路由时跳转到404,需要调整/anyone/visible/router逻辑,该接口将所有的菜单和视图都返回前端,并标记好哪些菜单无权限,然后前端控制跳转。

  3. 按钮权限。表格外的按钮通过自定义指令控制,表格操作栏、树操作栏、树右键菜单通过内部封装filter控制。

  4. 接口权限(又称URI、API),通过网关拦截器实现。参考: AuthenticationFilter

  5. 数据权限,通过数据权限拦截器DataScopeInnerInterceptor 动态拼接sql

权限控制原理简介

网关拦截器拦截所有请求,并判断用户是否拥有此请求的权限。
java
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        if (!ignoreProperties.getAuthEnabled()) {
            return chain.filter(exchange);
        }

    ServerHttpRequest request = exchange.getRequest();
    ServerHttpResponse response = exchange.getResponse();

    String eId = request.getHeaders().getFirst(ContextConstants.EMPLOYEE_ID_HEADER);
    String path = request.getPath().toString();
    String method = request.getMethodValue();
    // 1. 是否忽略验证 接口(URI、API)权限
    // 判断接口是否需要忽略token验证
    if (isIgnoreAuth(request)) {
        log.debug("当前接口:{}, 忽略权限验证", request.getPath());
        return chain.filter(exchange);
    }

    Long tenantId = ContextUtil.getTenantId();
    Long applicationId = ContextUtil.getApplicationId();
    Long employeeId = ContextUtil.getEmployeeId();

    // 2. 判断是否拥有该应用的权限
    Future<R<Boolean>> hasAppAsync = anyoneApi.checkApplication(applicationId, employeeId);
    R<Boolean> hasApp = hasAppAsync.get();
    if (!hasApp.getIsSuccess()) {
        return forbidden(response, hasApp.getMsg());
    } else if (hasApp.getData() == null || !hasApp.getData()) {
        return forbidden(response, UN_APPLICATION_AUTHORIZED);
    }

    // 3. 普通用户 需要校验 uri + method 的权限, 租户管理员 拥有分配给该企业的所有 资源权限
    Future<R<Boolean>> hasApiAsync = anyoneApi.checkUri(employeeId, applicationId, tenantId, path, method);
    R<Boolean> hasApi = hasApiAsync.get();
    if (!hasApi.getIsSuccess()) {
        return forbidden(response, hasApi.getMsg());
    } else if (hasApi.getData() == null || !hasApi.getData()) {
        return forbidden(response, UN_RESOURCE_AUTHORIZED);
    }
    ContextUtil.remove();
    return chain.filter(exchange);
}
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        if (!ignoreProperties.getAuthEnabled()) {
            return chain.filter(exchange);
        }

    ServerHttpRequest request = exchange.getRequest();
    ServerHttpResponse response = exchange.getResponse();

    String eId = request.getHeaders().getFirst(ContextConstants.EMPLOYEE_ID_HEADER);
    String path = request.getPath().toString();
    String method = request.getMethodValue();
    // 1. 是否忽略验证 接口(URI、API)权限
    // 判断接口是否需要忽略token验证
    if (isIgnoreAuth(request)) {
        log.debug("当前接口:{}, 忽略权限验证", request.getPath());
        return chain.filter(exchange);
    }

    Long tenantId = ContextUtil.getTenantId();
    Long applicationId = ContextUtil.getApplicationId();
    Long employeeId = ContextUtil.getEmployeeId();

    // 2. 判断是否拥有该应用的权限
    Future<R<Boolean>> hasAppAsync = anyoneApi.checkApplication(applicationId, employeeId);
    R<Boolean> hasApp = hasAppAsync.get();
    if (!hasApp.getIsSuccess()) {
        return forbidden(response, hasApp.getMsg());
    } else if (hasApp.getData() == null || !hasApp.getData()) {
        return forbidden(response, UN_APPLICATION_AUTHORIZED);
    }

    // 3. 普通用户 需要校验 uri + method 的权限, 租户管理员 拥有分配给该企业的所有 资源权限
    Future<R<Boolean>> hasApiAsync = anyoneApi.checkUri(employeeId, applicationId, tenantId, path, method);
    R<Boolean> hasApi = hasApiAsync.get();
    if (!hasApi.getIsSuccess()) {
        return forbidden(response, hasApi.getMsg());
    } else if (hasApi.getData() == null || !hasApi.getData()) {
        return forbidden(response, UN_RESOURCE_AUTHORIZED);
    }
    ContextUtil.remove();
    return chain.filter(exchange);
}
前端自定义指令
typescript
// src/directives/permission.ts 注册全局指令
/**
 * 注册全局 自定义指令
 */
export function setupPermissionDirective(app: App) {
  // 判断是否"拥有"指定的"所有"权限
  app.directive('auth', authDirective);
  // 判断是否"拥有"指定的"所有"权限
  app.directive('hasPermission', hasPermissionDirective);
  // 判断是否"没有"指定的"所有"权限
  app.directive('withoutPermission', withoutPermissionDirective);
  // 判断是否"没有"指定的"任意"权限
  app.directive('withoutAnyPermission', withoutAnyPermissionDirective);
  // 判断是否"拥有"指定的"任意"权限
  app.directive('hasAnyPermission', hasAnyPermissionDirective);
}
// src/directives/permission.ts 注册全局指令
/**
 * 注册全局 自定义指令
 */
export function setupPermissionDirective(app: App) {
  // 判断是否"拥有"指定的"所有"权限
  app.directive('auth', authDirective);
  // 判断是否"拥有"指定的"所有"权限
  app.directive('hasPermission', hasPermissionDirective);
  // 判断是否"没有"指定的"所有"权限
  app.directive('withoutPermission', withoutPermissionDirective);
  // 判断是否"没有"指定的"任意"权限
  app.directive('withoutAnyPermission', withoutAnyPermissionDirective);
  // 判断是否"拥有"指定的"任意"权限
  app.directive('hasAnyPermission', hasAnyPermissionDirective);
}
typescript
// src/hooks/web/usePermission.ts 权限判断逻辑 (代码比较复杂,运行项目后,debug更易理解)

// maxList是否包含minList
function containsAll(maxList: string[], minList: string[]) {
  return intersection(maxList, minList).length == minList.length;
}
/**
 * 判断是否有权限
 *
 * @param permissionsOwns  用户拥有的权限(后台/anyone/visible/resource接口查询出的权限集合)
 * @param toBeVerified     待验证的权限(按钮上面写死的资源编码,如<a-button v-hasPermission="['system:menu:delete']">)
 */
function isPermitted(permissionsOwns: WildcardPermission[], toBeVerified: WildcardPermission) {
  if (permissionsOwns == null || permissionsOwns.length === 0) {
    return false;
  }
  // 遍历用户拥有的权限,按个判断是否”包含“待验证的权限
  for (const owned of permissionsOwns) {
    if (owned.implies(toBeVerified)) {
      return true;
    }
  }
  return false;
}

const WILDCARD_TOKEN = '*'; // 通配符
const PART_DIVIDER_TOKEN = ':'; // 模块分隔符
const SUBPART_DIVIDER_TOKEN = ','; // 功能分隔符

/**
 * 通配符权限解析对象
 */
class WildcardPermission {
  // 解析后的只包含 : 的权限集合
  parts: string[][];

  /**
   * 将 wildcardString 解析存储到 parts
   *
   * @param wildcardString 原始通配符字符串
   * @param caseSensitive 是否区分大小写 true:区分;false:忽略大小写
   */
  constructor(wildcardString: string, caseSensitive: boolean) {
    this.parts = [];
    this._init_(wildcardString, caseSensitive);
  }

  // 解析通配符
  _init_(wildcardString: string, caseSensitive: boolean) {
    if (wildcardString == null || wildcardString.trim().length === 0) {
      throw new Error('权限编码通配符字符串不能为null或空。确保权限字符串的格式正确。');
    }
    wildcardString = wildcardString.trim();
    const parts: string[] = wildcardString.split(PART_DIVIDER_TOKEN);
    this.parts = [];
    for (const part of parts) {
      let subParts: string[] = part.split(SUBPART_DIVIDER_TOKEN);
      if (!caseSensitive) {
        const lowerSubParts: string[] = [];
        for (const subPart of subParts) {
          lowerSubParts.push(subPart.toLocaleLowerCase());
        }
        subParts = lowerSubParts;
      }
      if (subParts.length <= 0) {
        throw new Error(
          '权限编码通配符字符串不能包含只有分隔符的部分,确保权限编码字符串的格式正确。',
        );
      }
      this.parts.push(subParts);
    }

    if (this.parts.length <= 0) {
      throw new Error('权限编码通配符字符串不能只包含分隔符,确保权限编码字符串的格式正确。');
    }
  }

  // 真正的判断逻辑
  implies(toBeVerified: WildcardPermission) {
    const toBeVerifiedParts = toBeVerified.parts;
    let i = 0;
    for (const toBeVerifiedPart of toBeVerifiedParts) {
      // 如果此权限的部分数少于其他权限,则此权限中包含的部分数之后的所有内容都将自动隐含,因此返回true
      if (this.parts.length - 1 < i) {
        return false;
      } else {
        const part = this.parts[i];
        if (!part.includes(WILDCARD_TOKEN) && !containsAll(part, toBeVerifiedPart)) {
          return false;
        }
        i++;
      }
    }

    // 如果此权限的部分多于其他部分,则仅当所有其他部分都是通配符时才暗示它
    for (; i < this.parts.length; i++) {
      const part = this.parts[i];
      if (!part.includes(WILDCARD_TOKEN)) {
        return false;
      }
    }
    return true;
  }
}

const permMap = {};

    /**
   * 判断权限
   *
   * @param value 需要判断当前用户是否拥有的资源编码
   * @param def  value 为空时,默认是否拥有
   * @param mode 模式  可选值: 拥有所有 拥有任意 没有
   */
  function isPermission(
    value?: RoleEnum | RoleEnum[] | string | string[],
    def = true,
    mode = PermModeEnum.Has,
  ): boolean {
    // Visible by default
    if (!value) {
      return def;
    }

    const permMode = projectSetting.permissionMode;

    if ([PermissionModeEnum.ROUTE_MAPPING, PermissionModeEnum.ROLE].includes(permMode)) {
      if (!isArray(value)) {
        return userStore.getRoleList?.includes(value as RoleEnum);
      }
      return (intersection(value, userStore.getRoleList) as RoleEnum[]).length > 0;
    }

    if (PermissionModeEnum.BACK === permMode) {
      const visibleResource = permissionStore.getVisibleResource;
      const enabled = visibleResource?.enabled;
      if (!enabled) {
        return true;
      }

      let flag = true;
      if (mode === PermModeEnum.HasAny) {
        flag = false;
      }
      const resourceList = visibleResource.resourceList;
      const caseSensitive = visibleResource.caseSensitive;
      // 待校验权限 一定要是数组
      let permissions = value;
      if (!isArray(value)) {
        permissions = [value];
      }

      if (permissions != null && permissions.length > 0) {
        // 转换拥有的权限
        const permissionsOwns: WildcardPermission[] = [];
        for (const resource of resourceList) {
          permissionsOwns.push(new WildcardPermission(resource, caseSensitive));
        }

        for (const strPerm of permissions) {
          let toBeVerified;
          if (permMap[strPerm]) {
            toBeVerified = permMap[strPerm];
          } else {
            toBeVerified = new WildcardPermission(strPerm, caseSensitive);
          }

          // 不同的模式,校验规则不一样   
          if (mode === PermModeEnum.Has) {
            // 拥有所有权限
            if (!isPermitted(permissionsOwns, toBeVerified)) {
              flag = false;
            }
          } else if (mode === PermModeEnum.Without) {
            // 没有此权限
            if (isPermitted(permissionsOwns, toBeVerified)) {
              flag = false;
            }
          } else if (mode === PermModeEnum.HasAny) {
            // 拥有任意一个权限
            if (isPermitted(permissionsOwns, toBeVerified)) {
              flag = true;
            }
          } else if (mode === PermModeEnum.WithoutAny) {
            // 没有任意一个权限
            if (!isPermitted(permissionsOwns, toBeVerified)) {
              flag = true;
            }
          }
        }
      }

      return flag;
    }
    return true;
  }
// src/hooks/web/usePermission.ts 权限判断逻辑 (代码比较复杂,运行项目后,debug更易理解)

// maxList是否包含minList
function containsAll(maxList: string[], minList: string[]) {
  return intersection(maxList, minList).length == minList.length;
}
/**
 * 判断是否有权限
 *
 * @param permissionsOwns  用户拥有的权限(后台/anyone/visible/resource接口查询出的权限集合)
 * @param toBeVerified     待验证的权限(按钮上面写死的资源编码,如<a-button v-hasPermission="['system:menu:delete']">)
 */
function isPermitted(permissionsOwns: WildcardPermission[], toBeVerified: WildcardPermission) {
  if (permissionsOwns == null || permissionsOwns.length === 0) {
    return false;
  }
  // 遍历用户拥有的权限,按个判断是否”包含“待验证的权限
  for (const owned of permissionsOwns) {
    if (owned.implies(toBeVerified)) {
      return true;
    }
  }
  return false;
}

const WILDCARD_TOKEN = '*'; // 通配符
const PART_DIVIDER_TOKEN = ':'; // 模块分隔符
const SUBPART_DIVIDER_TOKEN = ','; // 功能分隔符

/**
 * 通配符权限解析对象
 */
class WildcardPermission {
  // 解析后的只包含 : 的权限集合
  parts: string[][];

  /**
   * 将 wildcardString 解析存储到 parts
   *
   * @param wildcardString 原始通配符字符串
   * @param caseSensitive 是否区分大小写 true:区分;false:忽略大小写
   */
  constructor(wildcardString: string, caseSensitive: boolean) {
    this.parts = [];
    this._init_(wildcardString, caseSensitive);
  }

  // 解析通配符
  _init_(wildcardString: string, caseSensitive: boolean) {
    if (wildcardString == null || wildcardString.trim().length === 0) {
      throw new Error('权限编码通配符字符串不能为null或空。确保权限字符串的格式正确。');
    }
    wildcardString = wildcardString.trim();
    const parts: string[] = wildcardString.split(PART_DIVIDER_TOKEN);
    this.parts = [];
    for (const part of parts) {
      let subParts: string[] = part.split(SUBPART_DIVIDER_TOKEN);
      if (!caseSensitive) {
        const lowerSubParts: string[] = [];
        for (const subPart of subParts) {
          lowerSubParts.push(subPart.toLocaleLowerCase());
        }
        subParts = lowerSubParts;
      }
      if (subParts.length <= 0) {
        throw new Error(
          '权限编码通配符字符串不能包含只有分隔符的部分,确保权限编码字符串的格式正确。',
        );
      }
      this.parts.push(subParts);
    }

    if (this.parts.length <= 0) {
      throw new Error('权限编码通配符字符串不能只包含分隔符,确保权限编码字符串的格式正确。');
    }
  }

  // 真正的判断逻辑
  implies(toBeVerified: WildcardPermission) {
    const toBeVerifiedParts = toBeVerified.parts;
    let i = 0;
    for (const toBeVerifiedPart of toBeVerifiedParts) {
      // 如果此权限的部分数少于其他权限,则此权限中包含的部分数之后的所有内容都将自动隐含,因此返回true
      if (this.parts.length - 1 < i) {
        return false;
      } else {
        const part = this.parts[i];
        if (!part.includes(WILDCARD_TOKEN) && !containsAll(part, toBeVerifiedPart)) {
          return false;
        }
        i++;
      }
    }

    // 如果此权限的部分多于其他部分,则仅当所有其他部分都是通配符时才暗示它
    for (; i < this.parts.length; i++) {
      const part = this.parts[i];
      if (!part.includes(WILDCARD_TOKEN)) {
        return false;
      }
    }
    return true;
  }
}

const permMap = {};

    /**
   * 判断权限
   *
   * @param value 需要判断当前用户是否拥有的资源编码
   * @param def  value 为空时,默认是否拥有
   * @param mode 模式  可选值: 拥有所有 拥有任意 没有
   */
  function isPermission(
    value?: RoleEnum | RoleEnum[] | string | string[],
    def = true,
    mode = PermModeEnum.Has,
  ): boolean {
    // Visible by default
    if (!value) {
      return def;
    }

    const permMode = projectSetting.permissionMode;

    if ([PermissionModeEnum.ROUTE_MAPPING, PermissionModeEnum.ROLE].includes(permMode)) {
      if (!isArray(value)) {
        return userStore.getRoleList?.includes(value as RoleEnum);
      }
      return (intersection(value, userStore.getRoleList) as RoleEnum[]).length > 0;
    }

    if (PermissionModeEnum.BACK === permMode) {
      const visibleResource = permissionStore.getVisibleResource;
      const enabled = visibleResource?.enabled;
      if (!enabled) {
        return true;
      }

      let flag = true;
      if (mode === PermModeEnum.HasAny) {
        flag = false;
      }
      const resourceList = visibleResource.resourceList;
      const caseSensitive = visibleResource.caseSensitive;
      // 待校验权限 一定要是数组
      let permissions = value;
      if (!isArray(value)) {
        permissions = [value];
      }

      if (permissions != null && permissions.length > 0) {
        // 转换拥有的权限
        const permissionsOwns: WildcardPermission[] = [];
        for (const resource of resourceList) {
          permissionsOwns.push(new WildcardPermission(resource, caseSensitive));
        }

        for (const strPerm of permissions) {
          let toBeVerified;
          if (permMap[strPerm]) {
            toBeVerified = permMap[strPerm];
          } else {
            toBeVerified = new WildcardPermission(strPerm, caseSensitive);
          }

          // 不同的模式,校验规则不一样   
          if (mode === PermModeEnum.Has) {
            // 拥有所有权限
            if (!isPermitted(permissionsOwns, toBeVerified)) {
              flag = false;
            }
          } else if (mode === PermModeEnum.Without) {
            // 没有此权限
            if (isPermitted(permissionsOwns, toBeVerified)) {
              flag = false;
            }
          } else if (mode === PermModeEnum.HasAny) {
            // 拥有任意一个权限
            if (isPermitted(permissionsOwns, toBeVerified)) {
              flag = true;
            }
          } else if (mode === PermModeEnum.WithoutAny) {
            // 没有任意一个权限
            if (!isPermitted(permissionsOwns, toBeVerified)) {
              flag = true;
            }
          }
        }
      }

      return flag;
    }
    return true;
  }

提示

以上权限判断逻辑借鉴了shiro的判断逻辑,支持英文符号 *:,,若在开发运营系统 -> 应用管理 -> 资源维护中配置了以下资源:

  • system:menu:*
  • system:*:add
  • system:menu,role:add
  • system:menu:add,edit,delete
  • system:file:download
  • system:file:upload;system:file:download

若同时给某个用户授权上面的所有权限,则该用户就拥有以下权限:

  • system:menu 模块下的所有操作权限,如system:menu:add、system:menu:update、system:menu:delete 等等
  • system模块的所有新增权限,如system:menu:add、system:role:add、system:file:add 等
  • system:menu:add和system:role:add
  • system:menu:add、system:menu:edit和system:menu:delete
  • system:file:download
  • system:file:upload和system:file:download

在代码中可以如下判断:

typescript
<a-button v-hasPermission="['system:menu:add']">新增菜单</a-button>  # 有(因为有: system:menu:*system:menu:add,edit,delete
<a-button v-hasPermission="['system:menu:delete']">删除菜单</a-button>  # 有 (因为有: system:menu:*system:menu:add,edit,delete
<a-button v-hasPermission="['system:menu:xxx']">xxx菜单</a-button>  # 有  (因为有: system:menu:*
<a-button v-hasPermission="['system:file:add']">新增文件</a-button>  # 有 (因为有: system:*:add)
<a-button v-hasPermission="['system:role:edit']">编辑角色</a-button>  # 无 
<a-button v-hasPermission="['system:role:import']">导入角色</a-button>  # 无
<a-button v-hasPermission="['system:menu:add']">新增菜单</a-button>  # 有(因为有: system:menu:*system:menu:add,edit,delete
<a-button v-hasPermission="['system:menu:delete']">删除菜单</a-button>  # 有 (因为有: system:menu:*system:menu:add,edit,delete
<a-button v-hasPermission="['system:menu:xxx']">xxx菜单</a-button>  # 有  (因为有: system:menu:*
<a-button v-hasPermission="['system:file:add']">新增文件</a-button>  # 有 (因为有: system:*:add)
<a-button v-hasPermission="['system:role:edit']">编辑角色</a-button>  # 无 
<a-button v-hasPermission="['system:role:import']">导入角色</a-button>  # 无
表格操作列filter过滤
typescript
// 参考: src/components/Table/src/components/TableAction.vue
const { isPermission } = usePermission();
// 操作列的普通按钮
const getActions = computed(() => {
  return (toRaw(props.actions) || [])
    .filter((action) => {
      // 通过配置的权限,过滤按钮
      return isPermission(action.auth, true, action.authMode) && isIfShow(action);
    })
    .map((action) => {
    const { popConfirm } = action;
    return {
      getPopupContainer: () => unref((table as any)?.wrapRef.value) ?? document.body,
                                     type: 'link',
                                     size: 'small',
                                     ...action,
                                     ...(popConfirm || {}),
        onConfirm: popConfirm?.confirm,
          onCancel: popConfirm?.cancel,
          enable: !!popConfirm,
  };
  });
});
// 操作列的下拉菜单的按钮
const getDropdownList = computed((): any[] => {
const list = (toRaw(props.dropDownActions) || []).filter((action) => {
  // 通过配置的权限,过滤按钮
  return isPermission(action.auth, true, action.authMode) && isIfShow(action);
});
return list.map((action, index) => {
  const { label, popConfirm } = action;
  return {
    ...action,
    ...popConfirm,
    onConfirm: popConfirm?.confirm,
    onCancel: popConfirm?.cancel,
    text: label,
    divider: index < list.length - 1 ? props.divider : false,
  };
 });
});
// 参考: src/components/Table/src/components/TableAction.vue
const { isPermission } = usePermission();
// 操作列的普通按钮
const getActions = computed(() => {
  return (toRaw(props.actions) || [])
    .filter((action) => {
      // 通过配置的权限,过滤按钮
      return isPermission(action.auth, true, action.authMode) && isIfShow(action);
    })
    .map((action) => {
    const { popConfirm } = action;
    return {
      getPopupContainer: () => unref((table as any)?.wrapRef.value) ?? document.body,
                                     type: 'link',
                                     size: 'small',
                                     ...action,
                                     ...(popConfirm || {}),
        onConfirm: popConfirm?.confirm,
          onCancel: popConfirm?.cancel,
          enable: !!popConfirm,
  };
  });
});
// 操作列的下拉菜单的按钮
const getDropdownList = computed((): any[] => {
const list = (toRaw(props.dropDownActions) || []).filter((action) => {
  // 通过配置的权限,过滤按钮
  return isPermission(action.auth, true, action.authMode) && isIfShow(action);
});
return list.map((action, index) => {
  const { label, popConfirm } = action;
  return {
    ...action,
    ...popConfirm,
    onConfirm: popConfirm?.confirm,
    onCancel: popConfirm?.cancel,
    text: label,
    divider: index < list.length - 1 ? props.divider : false,
  };
 });
});
表格字段列filter过滤
typescript
const { isPermission } = usePermission();

const getViewColumns = computed(() => {
  const viewColumns = sortFixedColumn(unref(getColumnsRef));

  const columns = cloneDeep(viewColumns);
  return columns
    .filter((column) => {
    if (column.auth) {
      // 通过配置的权限,过滤表格中的字段是否显示
      return isPermission(column.auth, true, column.authMode) && isIfShow(column);
    } else {
      return isIfShow(column);
    }
  })
    .map((column) => {
    //...
    return column;
  });
});
const { isPermission } = usePermission();

const getViewColumns = computed(() => {
  const viewColumns = sortFixedColumn(unref(getColumnsRef));

  const columns = cloneDeep(viewColumns);
  return columns
    .filter((column) => {
    if (column.auth) {
      // 通过配置的权限,过滤表格中的字段是否显示
      return isPermission(column.auth, true, column.authMode) && isIfShow(column);
    } else {
      return isIfShow(column);
    }
  })
    .map((column) => {
    //...
    return column;
  });
});
树操作列filter过滤
typescript
const { isPermission } = usePermission();
function renderAction(node: TreeItem) {
  const { actionList } = props;
  if (!actionList || actionList.length === 0) return;
  return actionList.map((item, index) => {
    let nodeShow = true;
    if (isFunction(item.show)) {
      nodeShow = item.show?.(node);
    } else if (isBoolean(item.show)) {
      nodeShow = item.show;
    }

    if (!nodeShow) return null;

    // 通过配置的权限,过滤树操作列按钮是否显示
    if (!isPermission(item.auth, true, item.authMode)) {
      return null;
    }

    return (
      <span key={index} class={bem('action')}>
    {item.render(node)}
      </span>
);
});
}
const { isPermission } = usePermission();
function renderAction(node: TreeItem) {
  const { actionList } = props;
  if (!actionList || actionList.length === 0) return;
  return actionList.map((item, index) => {
    let nodeShow = true;
    if (isFunction(item.show)) {
      nodeShow = item.show?.(node);
    } else if (isBoolean(item.show)) {
      nodeShow = item.show;
    }

    if (!nodeShow) return null;

    // 通过配置的权限,过滤树操作列按钮是否显示
    if (!isPermission(item.auth, true, item.authMode)) {
      return null;
    }

    return (
      <span key={index} class={bem('action')}>
    {item.render(node)}
      </span>
);
});
}
树右键菜单filter过滤
typescript
// src/components/ContextMenu/src/createContextMenu.ts
if (options.items) {
  const { isPermission } = usePermission();
  propsData.items = options.items.filter((item) => {
    return isPermission(item.auth, true, item.authMode);
  });
}
// src/components/ContextMenu/src/createContextMenu.ts
if (options.items) {
  const { isPermission } = usePermission();
  propsData.items = options.items.filter((item) => {
    return isPermission(item.auth, true, item.authMode);
  });
}

权限指令用法介绍

支持的指令:

  1. auth:(和hasPermission用法一致,建议使用hasPermission)判断是否"拥有"指定的"所有"权限

  2. hasPermission:判断是否"拥有"指定的"所有"权限。 支持数组和字符串参数

  3. withoutPermission:判断是否"没有"指定的"所有"权限。 支持数组和字符串参数

  4. hasAnyPermission:判断是否"拥有"指定的"任意"权限。支持数组和字符串参数

  5. withoutAnyPermission:判断是否"没有"指定的"任意"权限。 支持数组和字符串参数

    注意:指令的参数 不支持 * 和 , 分隔符, * 和 , 分隔符只能用在 资源维护 页面的 编码 字段!!!

    如:我在资源维护页配置了一个编码为:system:menu:* 的资源, 在判断权限时,v-hasPermission="system:menu:xxx" 将返回true,表示用户拥有xxx权限。

    用法:

    typescript
    // 判断用户是否 拥有 system:role:add 权限
    <a-button v-hasPermission="system:role:add">新增</a-button> 
    // 判断用户是否 拥有 system:menu:add 权限
    <a-button v-hasPermission="['system:menu:add']">新增菜单</a-button> 
    // 判断用户是否 同时拥有 system:menu:delete 和 system:menu:edit 权限
    <a-button v-hasPermission="['system:menu:delete', 'system:menu:edit']">删除编辑</a-button>  
    // 判断用户是否 同时没有 system:menu:delete 和 system:menu:edit 权限
    <a-button v-withoutPermission="['system:menu:delete', 'system:menu:edit']">xxx</a-button>   
    // 判断用户是否 拥有 system:menu:delete 或 system:menu:edit 任意一个权限
    <a-button v-hasAnyPermission="['system:menu:delete', 'system:menu:edit']">xxx</a-button>   
    // 判断用户是否 没有 system:menu:delete 或 system:menu:edit 任意一个权限
    <a-button v-withoutAnyPermission="['system:menu:delete', 'system:menu:edit']">xxx</a-button>
    // 判断用户是否 拥有 system:role:add 权限
    <a-button v-hasPermission="system:role:add">新增</a-button> 
    // 判断用户是否 拥有 system:menu:add 权限
    <a-button v-hasPermission="['system:menu:add']">新增菜单</a-button> 
    // 判断用户是否 同时拥有 system:menu:delete 和 system:menu:edit 权限
    <a-button v-hasPermission="['system:menu:delete', 'system:menu:edit']">删除编辑</a-button>  
    // 判断用户是否 同时没有 system:menu:delete 和 system:menu:edit 权限
    <a-button v-withoutPermission="['system:menu:delete', 'system:menu:edit']">xxx</a-button>   
    // 判断用户是否 拥有 system:menu:delete 或 system:menu:edit 任意一个权限
    <a-button v-hasAnyPermission="['system:menu:delete', 'system:menu:edit']">xxx</a-button>   
    // 判断用户是否 没有 system:menu:delete 或 system:menu:edit 任意一个权限
    <a-button v-withoutAnyPermission="['system:menu:delete', 'system:menu:edit']">xxx</a-button>

    同时支持 ***** 和 ,; 作为资源通配符(分隔符和通配符的规则按照shiro的规则来实现)。

    建议以view、add、edit、delete、export、import、download、upload等关键词结尾。

    如:authority:menu:add 只有菜单新增权限

    如:tenant:tenant:initConnect;tenant:datasourceConfig:view 租户初始化和数据源查询权限

    如:authority:resource:* 资源模块任意权限

    如:msg:sms:add,edit 短信功能的新增和修改权限

    权限属性用法介绍

    权限属性的实现方式基本都是通过以下两个参数来标记该字段或操作按钮需要什么权限,然后在通过filter方法中调用 usePermission.ts 中的isPermission方法来过滤。

  • auth?:string | string[] | RoleEnum | RoleEnum[]; # 待校验权限列表 可以是数组或字符串

  • authMode?:PermModeEnum; # 权限模式 可选值: Has、HasAny、Without、WithoutAny

  1. 表格操作列

    vue
    <TableAction
      // actions  右侧操作列按钮列表             
      :actions="[
        {
          auth: RoleEnum.MSG_MY_MSG_VIEW,  // 判断是否拥有 MSG_MY_MSG_VIEW 权限
          authMode: PermModeEnum.Has,         
        },
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.Has,  // 判断是否 同时拥有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限         
        },
      ]"
     // dropDownActions  右侧操作列更多下拉按钮列表          
      :dropDownActions="[
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.HasAny,  // 判断是否 拥有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
        },
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.Without,  // 判断是否 没有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限    
        },
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.WithoutAny, // 判断是否 没有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
        },
       ]"/>
    <TableAction
      // actions  右侧操作列按钮列表             
      :actions="[
        {
          auth: RoleEnum.MSG_MY_MSG_VIEW,  // 判断是否拥有 MSG_MY_MSG_VIEW 权限
          authMode: PermModeEnum.Has,         
        },
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.Has,  // 判断是否 同时拥有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限         
        },
      ]"
     // dropDownActions  右侧操作列更多下拉按钮列表          
      :dropDownActions="[
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.HasAny,  // 判断是否 拥有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
        },
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.Without,  // 判断是否 没有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限    
        },
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.WithoutAny, // 判断是否 没有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
        },
       ]"/>
  2. 表格字段

    typescript
    export const columns = (): BasicColumn[] => {
      return [
        {
          auth: RoleEnum.MSG_MY_MSG_VIEW,  // 判断是否拥有 MSG_MY_MSG_VIEW 权限
          authMode: PermModeEnum.Has,         
        },
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.Has,  // 判断是否 同时拥有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限         
        },
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.HasAny,  // 判断是否 拥有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
        },
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.Without,  // 判断是否 没有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限    
        },
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.WithoutAny, // 判断是否 没有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
        },
      ];
    };
    export const columns = (): BasicColumn[] => {
      return [
        {
          auth: RoleEnum.MSG_MY_MSG_VIEW,  // 判断是否拥有 MSG_MY_MSG_VIEW 权限
          authMode: PermModeEnum.Has,         
        },
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.Has,  // 判断是否 同时拥有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限         
        },
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.HasAny,  // 判断是否 拥有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
        },
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.Without,  // 判断是否 没有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限    
        },
        {
          auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
          authMode: PermModeEnum.WithoutAny, // 判断是否 没有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
        },
      ];
    };
  3. 树操作列(悬停图标)

    typescript
    // 悬停图标
    const actionList: TreeActionItem[] = [
      {
        auth: RoleEnum.ORG_ADD,
        authMode: PermModeEnum.Has,  // 判断是否 拥有 ORG_ADD 权限       
        render: (node) => { return h('a', {}, '新增'); },
      },
      {
        auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
        authMode: PermModeEnum.Has,  // 判断是否 同时拥有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限         
      },
      {
        auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
        authMode: PermModeEnum.HasAny,  // 判断是否 拥有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
      },
      {
        auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
        authMode: PermModeEnum.Without,  // 判断是否 没有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限    
      },
      {
        auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
        authMode: PermModeEnum.WithoutAny, // 判断是否 没有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
      },      
    ];
    // 悬停图标
    const actionList: TreeActionItem[] = [
      {
        auth: RoleEnum.ORG_ADD,
        authMode: PermModeEnum.Has,  // 判断是否 拥有 ORG_ADD 权限       
        render: (node) => { return h('a', {}, '新增'); },
      },
      {
        auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
        authMode: PermModeEnum.Has,  // 判断是否 同时拥有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限         
      },
      {
        auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
        authMode: PermModeEnum.HasAny,  // 判断是否 拥有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
      },
      {
        auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
        authMode: PermModeEnum.Without,  // 判断是否 没有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限    
      },
      {
        auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
        authMode: PermModeEnum.WithoutAny, // 判断是否 没有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
      },      
    ];
  4. 树右键菜单

    typescript
    // 右键菜单
    function getRightMenuList(node: any): ContextMenuItem[] {
    return [
     {
     auth: [RoleEnum.ORG_ADD],
     authMode: PermModeEnum.Has,  // 判断是否 拥有 ORG_ADD 权限      
     },
     {
     auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
     authMode: PermModeEnum.Has,  // 判断是否 同时拥有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限         
     },
     {
     auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
     authMode: PermModeEnum.HasAny,  // 判断是否 拥有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
     },
     {
     auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
     authMode: PermModeEnum.Without,  // 判断是否 没有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限    
     },
     {
     auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
     authMode: PermModeEnum.WithoutAny, // 判断是否 没有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
     },
     ];
    }
    // 右键菜单
    function getRightMenuList(node: any): ContextMenuItem[] {
    return [
     {
     auth: [RoleEnum.ORG_ADD],
     authMode: PermModeEnum.Has,  // 判断是否 拥有 ORG_ADD 权限      
     },
     {
     auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
     authMode: PermModeEnum.Has,  // 判断是否 同时拥有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限         
     },
     {
     auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
     authMode: PermModeEnum.HasAny,  // 判断是否 拥有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
     },
     {
     auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
     authMode: PermModeEnum.Without,  // 判断是否 没有 MSG_MY_MSG_VIEW 和 MSG_MY_MSG_DELETE 权限    
     },
     {
     auth: [RoleEnum.MSG_MY_MSG_VIEW, RoleEnum.MSG_MY_MSG_DELETE], 
     authMode: PermModeEnum.WithoutAny, // 判断是否 没有 MSG_MY_MSG_VIEW 或 MSG_MY_MSG_DELETE 任意一个权限    
     },
     ];
    }

开发流程

大致流程为:每次新开发一个页面或接口,都需要先配置资源,在让开发者给企业授权,最后在让企业给自己内部员工授权

  1. 配置资源:开发者先在开发运营系统 - 应用管理 - 资源维护页面配置资源。

  2. 编写按钮权限控制代码:在需要控制权限的按钮、字段上编写权限指令权限属性

    • 资源维护页面配置资源时,资源的类型是由你们的业务决定的!如:发送消息(新增),若你希望弹窗后发送消息,可以将类型设置为功能,但希望新开tab页,则一定要将类型设置为视图

    • 在代码层面给按钮配置权限时,可以配置视图功能类型的资源。

  3. 开发者给企业授权:开发者在开发运营系统 - 应用管理 - 应用授权管理页面,将新配置的资源授权给某些企业。

  4. 企业给自己内部员工授权:租户管理员在基础平台 - 系统功能 - 角色权限维护 页面先配置角色拥有的权限,然后绑定用户

  5. 或者 租户管理员在基础平台 - 用户中心 - 员工管理 页面先 绑定角色

常见问题

  1. 为什么租户管理员也报错:"对不起,您无该URI资源的权限"?

答: 在”资源维护“页面没配置的接口,任何人都没权限访问! 租户管理员(TENANT_ADMIN)默认拥有所有已经 在【资源维护】页面配置的资源,并在【应用资源授权】页面授权 给指定企业后,租户管理员即可拥有权限。 普通用户需要 租户管理员 在【角色权限维护】页面授权后,才有权限。

欢迎使用天源云Saas快速开发系统