[JavaWeb] You are so awesome, do you know the RBAC permission model and ABAC permission model?

Permissions are a relatively common basic function in daily systems. Login is required for systems with permission modules 用户能够操作哪些资源,不能够操作哪些资源. With the help of the permission module, we can effectively control the specific operations performed by people with different identities in the system. It can be said that a mature back-end system is inseparable from a relatively complete permission management system.

So the goal of the permission control system is:管理用户行为,保护系统功能。

So how to perform permission control?

  • Define resources
  • Create permissions
  • Creating a Role
  • Manage users
  • Establish an association

1. RBAC permission model

1.Composition of RBAC

  • In the RBAC model, there are three basic components, namely:用户、角色和权限
    Insert image description here
  • User : Each user is identified by a unique UID and is granted different roles
  • Role : Different roles have different permissions
  • Permission : access rights
  • User-role mapping : the mapping relationship between users and roles
  • Role-permission mapping : mapping between roles and permissions

    Role permission relationship

    • Permission → Resource: One-way many-to-many. One permission can contain multiple resources, and one resource can be assigned to multiple different permissions.
    • Role → Permission: One-way many-to-many. One role can contain multiple permissions, and one permission can be assigned to multiple different roles.
    • User → Role: Two-way many-to-many. One role can contain multiple users, and one user can hold multiple positions.

For example, administrators and ordinary users are granted different permissions. Ordinary users can only modify and view personal information, but cannot create users or freeze users. Administrators can do all operations because they are granted all permissions.
Insert image description here

2.RBAC model classification

2.1.Basic model RBAC0

RBAC0 is the foundation , and many systems can build permission models based only on RBAC0.

  • In this model, we 把权限赋予角色,再把角色赋予用户. Users and roles, roles and permissions are 多对多all related. The permissions a user has are equal to the sum of the permissions held by all his roles.
    Insert image description here

To give an example:

  • For example, if we make an enterprise management product, we can 抽象出几个角色, for example, sales managers, financial managers, marketing managers , etc., and then 把权限分配给这些角色,再把角色赋予用户. In this way, whether it is assigning permissions or modifying permissions in the future , you only need to modify them 用户和角色的关系,或角色和权限的关系, which is more flexible and convenient.
  • In addition, if a user has multiple roles, and Mr. Wang is responsible for both the sales department and the marketing department , then Mr. Wang can be given the roles of 2个角色sales manager and marketing manager, so that he has both 这两个角色的所有权限.

2.2. Role hierarchical model RBAC1

RBAC1 is based on RBAC0 在角色中引入了 "继承" 的概念. The simple understanding is, 给角色可以分成几个等级,每个等级权限不同,从而实现更细粒度的权限管理.
Insert image description here
To give an example:

  • Based on the previous example of RBAC0, we also found that the sales manager of a company may be divided into several levels. For example, in addition to the sales manager, there is also a deputy sales manager, and the deputy sales manager only has part of the authority of the sales manager. At this time, we can use the RBAC1 grading model to divide the role of sales manager into multiple levels, and assign a lower level to the deputy sales manager.

2.3. Role restriction model RBAC2

RBAC2 is also built on RBAC0the foundation, but adds some restrictions on users, roles and permissions.

  • These restrictions can be divided into two categories, namely 静态职责分离SSD(Static Separation of Duty) and 动态职责分离DSD(Dynamic Separation of Duty). The specific restrictions are as follows:
    Insert image description here

To give an example:

  • Still based on the previous example of RBAC0, we found that some roles are 互斥needed
    • For example, if a user is assigned the role of sales manager, he cannot be assigned the role of financial manager, otherwise he 录入合同can审核合同
      • For another example, some companies attach great importance to role upgrades. If a salesperson wants to be upgraded to a sales manager, he must first be upgraded to a sales supervisor. At this time, it should be adopted 先决条件限制.

2.4. Unified model RBAC3

RBAC3 is RBAC1和RBAC2的合集, so RBAC3 has both ** 角色分层and OK 增加各种限制. **
Insert image description here

3.RBAC0 model core table structure

3.1.Table structure design

  • From the above entity correspondence analysis, the permission table design is divided into the following five basic table structures: user table (t_user), role table (t_role), t_user_role (user role table), resource table (t_module), permission table (t_permission) , the table structure relationship is as follows:
    Insert image description here
t_user	用户表			
	主键	id	int(11)	自增	主键id
	user_name	varchar(20)	非空	用户名
	user_pwd	varchar(100)	非空	用户密码
	true_name	varchar(20)	可空	真实姓名
	email	varchar(30)	可空	邮箱
	phone	varchar(20)	可空	电话
	is_valid	int(4)	可空	有效状态
	create_date	datetime	可空	创建时间
	update_date	datetime	可空	更新时间

t_role	角色表			
	主键	id	int(11)	自增	主键id
	role_name	varchar(20)	非空	角色名
	role_remarker	varchar(100)	可空	角色备注
	is_valid	int(4)	可空	有效状态
	create_date	datetime	可空	创建时间

t_user_role	用户角色表			
	主键	id	int(11)	自增	主键id
	user_id	int(4)	非空	用户id
	role_id	int(4)	角色id	角色id
	create_date	datetime	可空	创建时间
	update_date	datetime	可空	更新时间

t_module	资源表			
	主键	id	int(11)	自增	资源id
	module_name	varchar(20)	可空	资源名
	module_style	varchar(100)	可空	资源样式
	url	varchar(20)	可空	资源url地址
	parent_id	int(11)	非空	上级资源id
	parent_opt_value	varchar(20)	非空	上级资源权限码
	grade	int(11)	非空	层级
	opt_value	varchar(30)	可空	权限码
	orders	int(11)	非空	排序号
	is_valid	int(4)	可空	有效状态
	create_date	datetime	可空	创建时间
	update_date	datetime	可空	更新时间
	
t_permission	权限表			
	主键	id	int(11)	自增	主键id
	role_id	int(11)	非空	角色id
	module_id	int(11)	非空	资源id
	acl_value	varchar(20)	非空	权限码
	create_date	datetime	可空	创建时间
	update_date	datetime	可空	更新时间

3.2. Module division

It can be seen from the table structure design: there are three main tables (t_user, t_role, t_module), which are divided into three major modules in terms of function implementation:

User Management

  • Maintenance of basic user information
  • User role assignment

role management

  • Maintenance of basic role information
  • Role authorization (assign menus that can be operated by roles)
  • Role authentication (verify the permissions owned by the role)

Resource management

  • Resource information maintenance
  • Menu output dynamic control

4. RBAC-based extension—user group

  • Based on the RBAC model, it can also be appropriately extended to make it more suitable for our products. For example 增加用户组概念, directly to the user 户组分配角色,再把用户加入用户group. In this way, in addition to having their own permissions, the user also has all the permissions of the user group to which he belongs.
    Insert image description here
    To give an example:

  • For example, we can combine one 部门看成一个用户组, such as the sales department and the finance department, and then give 这个部门直接赋予角色,使部门拥有部门权限,这样这个部门的所有用户都有了部门权限。the user group concept to more conveniently authorize group users without affecting the role permissions that the users already have.

2. ABAC permission model (explanation based on Java)

1.What is ABAC

ABAC(Attribute Base Access Control)

  • Based 属性on permission control, which is different from the common way of associating users with permissions in some way, ABAC makes authorization judgments by dynamically calculating one or a group of attributes to determine whether certain conditions are met (simple logic can be written).
  • Attributes are generally divided into four categories: 用户属性(如用户年龄), 环境属性(如当前时间), 操作属性(如读取)和对象属性, so in theory, very flexible permission control can be achieved and can meet almost all types of needs.

2. ABAC conditional judgment control

Based on ABAC access control requirements 要动态计算实体的属性、操作类型、相关的环境来控制是否有对操作对象的权限, the flexibility, versatility, and ease of use of conditional judgment need to be considered during design. Users only need to configure authorization through the web page, which requires reducing hard-coding and making the logic simple and universal. , then this needs to satisfy some operators to achieve.

type operator
arithmetic operators +, -, *, /, %, ^, div, mod
Relational operators <, >, ==, !=, <=, >=, lt, gt, eq, ne, le, ge
Logical Operators and, or, not, &&,
condition ?:
  • For example: Users only need to configure the condition of user.age > 20 to obtain specific permissions.

3. Expression Language (EL)

As mentioned in the previous section, if you need to parse a certain condition, you need an expression language. Using the expression language, you can easily access the attributes in the object, or perform various mathematical operations, conditional judgments, etc.

As: Spring Framework的@ValueAnnotations and MyBatis<if test=“”>

// 相信很多 Java boy都使用过的吧
@Value("A?B:C")
private String A;
<select id = "XXX">
    <if test="user != null">
        XXXX
    </if>
</select

Therefore 此ABAC的的核心就是Expression Language(EL), although the demonstration is used by Java as a demonstration, other programming languages ​​​​have their own EL frameworks.

java EL framework list

  • spring-expression

  • OGNL

  • MVEL

  • JBoss EL

If you are interested, you can check the Java EL ecological ranking: https://mvnrepository.com/open-source/expression-languages

4.ABAC practice

4.1. Database design

Insert image description here
Insert image description here

# 用户表
DROP TABLE IF EXISTS USER;
CREATE TABLE USER (
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    NAME VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    age INT(11) NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id) COMMENT '用户表'
);
# 用户边缘数据
DROP TABLE IF EXISTS user_contribution;
CREATE TABLE user_contribution (
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    user_id BIGINT(20)  NOT NULL COMMENT '用户表ID',
    repository VARCHAR(100) NOT NULL COMMENT '仓库',
    PRIMARY KEY (id) COMMENT '用户边缘数据'
);
# 权限表
DROP TABLE IF EXISTS permission;
CREATE TABLE permission (
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    permission VARCHAR(100) NOT NULL COMMENT '权限名称',
    PRIMARY KEY (id) COMMENT '权限表'
);
# abac表达式表
DROP TABLE IF EXISTS abac;
CREATE TABLE abac (
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    expression VARCHAR(100) NOT NULL COMMENT 'abac表达式',
    PRIMARY KEY (id) COMMENT 'abac表达式表'
);
# abac表和权限表的关联表, o2m
DROP TABLE IF EXISTS abac_permission;
CREATE TABLE abac_permission (
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    abac_id BIGINT(20) NOT NULL COMMENT 'abac表ID',
    permission_id BIGINT(20) NOT NULL COMMENT 'permission表ID',
    PRIMARY KEY (id) COMMENT 'abac表和权限表的关联表, o2m'
);


#插入用户数据
DELETE FROM USER;
INSERT INTO USER (id, NAME, age, email)
VALUES (1, '魏昌进', 26, '[email protected]'),
       (2, 'test', 1, '[email protected]'),
       (3, 'admin', 1, '[email protected]');
 
 #插入用户边缘数据
DELETE FROM user_contribution;
INSERT INTO user_contribution (id, user_id, repository)
VALUES (1, 1, 'galaxy-sea/spring-cloud-apisix'),
       (2, 2, 'spring-cloud/spring-cloud-commons'),
       (3, 2, 'spring-cloud/spring-cloud-openfeign'),
       (4, 2, 'alibaba/spring-cloud-alibaba'),
       (5, 2, 'Tencent/spring-cloud-tencent'),
       (6, 2, 'apache/apisix-docker');
       
#插入权限数据
DELETE FROM permission;
INSERT INTO permission (id, permission)
VALUES (1, 'github:pr:merge'),
       (2, 'github:pr:close'),
       (3, 'github:pr:open'),
       (4, 'github:pr:comment');
 
#插入abac表达式数据 
DELETE FROM abac;
INSERT INTO abac (id, expression)
VALUES (1, 'contributions.contains(''galaxy-sea/spring-cloud-apisix'')'),
       (2, 'name == ''admin'''),
       (3, 'metadata.get(''ip'') == ''192.168.0.1''');
 
#插入abac表达式-权限映射关系数据  
DELETE FROM abac_permission;
INSERT INTO abac_permission (id, abac_id, permission_id)
VALUES (1, 1, 1),
       (2, 2, 1),
       (3, 2, 2),
       (4, 2, 3),
       (5, 2, 4),
       (6, 3, 1),
       (7, 3, 2),
       (8, 3, 3),
       (9, 3, 4);

4.2.Introducing dependencies

This chapter only implements the principles of ABAC and will not do Spring Security和 Apache Shiroany integration

frame

  • Java 8
  • Spring Boot 2.x
  • MyBatis Plus 3.5.x
  • MySQL 8.0
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>abac</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-abac</name>
    <description>springboot-abac</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>compile</scope>
        </dependency>

        <!-- security权限依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

4.3. Modify configuration

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      password: root
      username: root
      driver-class-name: com.mysql.cj.jdbc.Driver
      #数据库名字自己定义
      url: jdbc:mysql://127.0.0.1:3306/abac_test?serverTimezone=UTC&useUnicode=true&charaterEncoding=utf-8&useSSL=false
      validation-query: SELECT 1


mybatis-plus:
  #配置 Maaper xml文件所在路径
  mapper-locations: classpath*:/mapper/**/*.xml
  #配置映射类所在的包名
  type-aliases-package: com.example.entity

server:
  port: 8090

4.4.CRUD code

entiy

@Data
public class Abac {
    
    
    private Long id;
    private String expression;
    /**
     * expression对应的permissions列表
     */
    @TableField(exist = false)
    private List<String> permissions;
}
@Data
public class User {
    
    
    private Long id;
    private String name;
    private Integer age;
    private String email;

    /** 用户提交过仓库 */
    @TableField(exist = false)
    private List<String> contributions = new ArrayList<>();

    /** 存放一些乱七八糟的数据,当然contributions字段也放在这里 */
    @TableField(exist = false)
    private Map<String, Object> metadata = new HashMap<>();
}

layer

@Mapper
public interface AbacDao extends BaseMapper<Abac> {
    
    
    /** 获取abacId关联权限 */
    @Select("SELECT p.permission\n" +
            "FROM abac_permission ap LEFT JOIN permission p ON p.id = ap.permission_id\n" +
            "WHERE ap.abac_id = #{abacId}")
    List<String> selectPermissions(Long abacId);
}
@Mapper
public interface UserDao extends BaseMapper<User> {
    
    
    /** 获取用户的仓库信息 */
    @Select("SELECT repository FROM user_contribution WHERE user_id = #{userId}")
    List<String> selectRepository(@Param("userId") Long userId);
}

service layer

@Service
@RequiredArgsConstructor
public class AbacService {
    
    
    private final AbacDao abacDao;

    /** 获取abac表达式详细信息列表 */
    public List<Abac> getAll() {
    
    
        List<Abac> abacs = abacDao.selectList(null);
        for (Abac abac : abacs) {
    
    
            List<String> permissions = abacDao.selectPermissions(abac.getId());
            abac.setPermissions(permissions);
        }
        return abacs;
    }
}
@Service
@RequiredArgsConstructor
public class UserService {
    
    
    private final UserDao userDao;

    /** 根据userId获取用户详细信息 */
    public User get(Long userId) {
    
    
        User user = userDao.selectById(userId);
        List<String> repository = userDao.selectRepository(userId);
        user.setContributions(repository);
        return user;
    }
}

4.5.security context

/**
 * 自定义用户元数据  用于获取一些实体的属性、操作类型、相关的环境
 */
public interface MetadataCustomizer {
    
    

    /** 自定义用户元数据 */
    void customize(User user);
}
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 解析abac表达式
 */
@Component
public class SecurityContext {
    
    
    /**
     * SpEL表达式解析器
     */
    private final ExpressionParser expressionParser = new SpelExpressionParser();

    /**
     * 解析abac表达式
     *
     * @param user  用户详细信息
     * @param abacs abac表达式详细信息集合
     * @return expressions集合, 将这个结果集存放到 Spring Security 或者Apache APISIX的userDetail上下文中
     */
    public List<String> rbacPermissions(User user, List<Abac> abacs) {
    
    
        return this.rbacPermissions(user, abacs, Collections.emptyList());
    }

    /**
     * 解析abac表达式
     *
     * @param user                用户详细信息
     * @param abacs               abac表达式详细信息集合
     * @param metadataCustomizers 自定义用户元数据  用于获取一些实体的属性、操作类型、相关的环境
     * @return expressions集合, 将这个结果集存放到 Spring Security 或者Apache APISIX的userDetail上下文中
     */
    public List<String> rbacPermissions(User user, List<Abac> abacs, List<MetadataCustomizer> metadataCustomizers) {
    
    
        // 处理自定义元数据
        metadataCustomizers.forEach(metadataCustomizer -> metadataCustomizer.customize(user));

        List<String> expressions = new ArrayList<>();
        for (Abac abac : abacs) {
    
    
            // 解析表达式的求值器
            Expression expression = expressionParser.parseExpression(abac.getExpression());
            // 创建环境上下文
            EvaluationContext context = new StandardEvaluationContext(user);
            // 获取expression的结果
            if (expression.getValue(context, boolean.class)) {
    
    
                expressions.addAll(abac.getPermissions());
            }
        }
        return expressions;
    }
}

4.6.Startup class

@SpringBootApplication
public class SpringbootAbacApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(SpringbootAbacApplication.class, args);
    }
}

4.7. Test class

@SpringBootTest
class AbacApplicationTests {
    
    

    @Autowired
    private UserService userService;

    @Autowired
    private AbacService abacService;

    @Autowired
    private SecurityContext securityContext;

    /** 获取不同用户的abac权限 */
    @Test
    void testRbac() {
    
    
        User user = userService.get(1L);
        System.out.println(user);

        List<Abac> rbac = abacService.getAll();
        System.out.println(rbac);

        List<String> permissions = securityContext.rbacPermissions(user, rbac);
        System.out.println(permissions);

        user = userService.get(2L);
        System.out.println(user);
        permissions = securityContext.rbacPermissions(user, rbac);
        System.out.println(permissions);

        user = userService.get(3L);
        System.out.println(user);
        permissions = securityContext.rbacPermissions(user, rbac);
        System.out.println(permissions);

    }

    /**
     * 获取自定义权限
     */
    @Test
    void testMetadataCustomizer() {
    
    
        User user = userService.get(1L);
        System.out.println(user);

        List<Abac> rbac = abacService.getAll();
        System.out.println(rbac);

        List<String> permissions = securityContext.rbacPermissions(user, rbac);
        System.out.println(permissions);

        permissions = securityContext.rbacPermissions(user, rbac, getMetadataCustomizer());
        System.out.println(permissions);
    }

    /** 模拟网络ip */
    private List<MetadataCustomizer> getMetadataCustomizer() {
    
    
        return new ArrayList<MetadataCustomizer>() {
    
    {
    
    
            add(user -> user.getMetadata().put("ip", "192.168.0.1"));
        }};
    }
}

testRbac() test effect

#用户1
User(id=1, name=魏昌进, age=26, email=[email protected], contributions=[galaxy-sea/spring-cloud-apisix], metadata={
    
    })
#所有ABAC
[Abac(id=1, expression=contributions.contains('galaxy-sea/spring-cloud-apisix'), permissions=[github:pr:merge]), Abac(id=2, expression=name == 'admin', permissions=[github:pr:merge, github:pr:close, github:pr:open, github:pr:comment]), Abac(id=3, expression=metadata.get('ip') == '192.168.0.1', permissions=[github:pr:merge, github:pr:close, github:pr:open, github:pr:comment])]
#用户1权限
[github:pr:merge]

#用户2
User(id=2, name=test, age=1, email=[email protected], contributions=[spring-cloud/spring-cloud-commons, spring-cloud/spring-cloud-openfeign, alibaba/spring-cloud-alibaba, Tencent/spring-cloud-tencent, apache/apisix-docker], metadata={
    
    })
#用户2权限
[]

#用户3
User(id=3, name=admin, age=1, email=[email protected], contributions=[], metadata={
    
    })
#用户3权限
[github:pr:merge, github:pr:close, github:pr:open, github:pr:comment]

testMetadataCustomizer() test effect

#用户1
User(id=1, name=魏昌进, age=26, email=[email protected], contributions=[galaxy-sea/spring-cloud-apisix], metadata={
    
    })
#所有ABAC
[Abac(id=1, expression=contributions.contains('galaxy-sea/spring-cloud-apisix'), permissions=[github:pr:merge]), Abac(id=2, expression=name == 'admin', permissions=[github:pr:merge, github:pr:close, github:pr:open, github:pr:comment]), Abac(id=3, expression=metadata.get('ip') == '192.168.0.1', permissions=[github:pr:merge, github:pr:close, github:pr:open, github:pr:comment])]
#解析abac表达式,获取用户1满足条件的权限
[github:pr:merge]
#自定义用户元数据 ,判断用户1是否满足ip环境权限
[github:pr:merge, github:pr:merge, github:pr:close, github:pr:open, github:pr:comment]

5.Spring Security and Apache Shiro integration

Spring Security only needs to modify the interceptor and UserDetailsconvert it SecurityContext#rbacPermissionstoGrantedAuthority

/**
 * 这里是伪代码, 展示一下大概逻辑
 */
public class IamOncePerRequestFilter implements OncePerRequestFilter {
    
    
 
  @Autowired
  private SecurityContext securityContext;
 
  @Autowired
  private AbacService abacService;
 
  @Autowired
  private List<MetadataCustomizer> metadataCustomizers;
 
  @Autowired
  public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
    
    
    UserDetails user = toUser();
    List<String> permissions = securityContext.rbacPermissions(user, abacService.getAll(), metadataCustomizers);
    List<GrantedAuthority> abacAuthority = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    user.getAuthorities().addAll(abacAuthority);
  }
}

Guess you like

Origin blog.csdn.net/qq877728715/article/details/133168814