There are many ways to refine the granularity of permission control. This article follows on from the previous article (how to refine permission granularity in Spring Security?) , through a specific case to show friends based on Acl permission control. Other permission control models will be introduced later.

1. Preparation

First, create a Spring Boot project, because we are involved in database operations, so in addition to Spring Security dependency, also need to add database driver and MyBatis dependency.

Since there is no ACL-related starter, we need to manually add acl dependencies. In addition, ACLs also rely on ehcache, so we need to add cache dependencies.

The final POM.xml file looks like this:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-acl</artifactId>
    <version>5.3.4. RELEASE</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.10.4</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.23</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>
Copy the code

After the project is successfully created, we can find the database script file in the JAR package of the ACL:

Select an appropriate script to execute according to your database, and create a total of four tables as follows:

I will not explain the meaning of the table too much, but you can refer to the previous article: How to refine the permission granularity in Spring Security?

Finally, configure the database information in the project’s application.properties file as follows:

spring.datasource.url=jdbc:mysql:///acls? useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
Copy the code

At this point, the preparation is complete. Next, let’s look at configuration.

2. The ACL configuration

This configuration code is quite large, I put the code first, we will analyze one by one:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AclConfig {

    @Autowired
    DataSource dataSource;

    @Bean
    public AclAuthorizationStrategy aclAuthorizationStrategy(a) {
        return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ADMIN"));
    }

    @Bean
    public PermissionGrantingStrategy permissionGrantingStrategy(a) {
        return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
    }

    @Bean
    public AclCache aclCache(a) {
        return new EhCacheBasedAclCache(aclEhCacheFactoryBean().getObject(), permissionGrantingStrategy(), aclAuthorizationStrategy());
    }

    @Bean
    public EhCacheFactoryBean aclEhCacheFactoryBean(a) {
        EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
        ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject());
        ehCacheFactoryBean.setCacheName("aclCache");
        return ehCacheFactoryBean;
    }

    @Bean
    public EhCacheManagerFactoryBean aclCacheManager(a) {
        return new EhCacheManagerFactoryBean();
    }

    @Bean
    public LookupStrategy lookupStrategy(a) {
        return new BasicLookupStrategy(dataSource, aclCache(), aclAuthorizationStrategy(), new ConsoleAuditLogger()
        );
    }

    @Bean
    public AclService aclService(a) {
        return new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache());
    }

    @Bean
    PermissionEvaluator permissionEvaluator(a) {
        AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService());
        returnpermissionEvaluator; }}Copy the code
  1. Open project @ @ EnableGlobalMethodSecurity annotation configuration said PreAuthorize, @ PostAuthorize and @ Secured the use of annotations, we want to through these notes configuration access for a while.
  2. Because the whole database thing is introduced and the database connection information is configured, the DataSource instance can be injected here for later use.
  3. The AclAuthorizationStrategy instance is used to determine whether the current authentication principal has the permission to modify the Acl. To be precise, there are three permissions: to modify the owner of the Acl; Modify the audit information about acLs and the ACE itself. This interface is only one implementation class is AclAuthorizationStrategyImpl, when we create an instance, can be introduced to three parameters, respectively corresponding to the three kinds of permissions, can also pass in a parameter, said that a character can do three things.
  4. PermissionGrantingStrategy interface provides a isGranted method, this method is the ultimate approach to authority than real, the only one interface implementation class DefaultPermissionGrantingStrategy, I’ll just go new.
  5. In ACL system, because the permission comparison always needs to query the database, resulting in performance problems, so Ehcache is introduced to cache. AclCache has two implementation classes: SpringCacheBasedAclCache and EhCacheBasedAclCache. We have already introduced the EhCache instance, so we can configure the EhCacheBasedAclCache instance.
  6. LookupStrategy can resolve the Acl using ObjectIdentity. LookupStrategy has only one implementation class, BasicLookupStrategy, which is new.
  7. AclService is a service that we have already introduced, so we will not repeat it here.
  8. PermissionEvaluator is supported for the expression hasPermission. Since the later use of this case is similar to@PreAuthorize("hasPermission(#noticeMessage, 'WRITE')")Such annotations are for permission control, so you need to configure an instance of PermissionEvaluator.

At this point, the configuration class here and you are finished.

3. Plot setting

Suppose I now have a notification message class of NoticeMessage like this:

public class NoticeMessage {
    private Integer id;
    private String content;

    @Override
    public String toString(a) {
        return "NoticeMessage{" +
                "id=" + id +
                ", content='" + content + '\' ' +
                '} ';
    }

    public Integer getId(a) {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getContent(a) {
        return content;
    }

    public void setContent(String content) {
        this.content = content; }}Copy the code

Then create a table based on the class:

CREATE TABLE `system_message` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `content` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL.PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Copy the code

Then the next access control is for this NoticeMessage.

Create NoticeMessageMapper and add a few test methods:

@Mapper
public interface NoticeMessageMapper {
    List<NoticeMessage> findAll(a);

    NoticeMessage findById(Integer id);

    void save(NoticeMessage noticeMessage);

    void update(NoticeMessage noticeMessage);
}
Copy the code

Noticemessagemapper.xml reads as follows:

<! DOCTYPEmapper
        PUBLIC "- / / mybatis.org//DTD Mapper / 3.0 / EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.javaboy.acls.mapper.NoticeMessageMapper">


    <select id="findAll" resultType="org.javaboy.acls.model.NoticeMessage">
        select * from system_message;
    </select>

    <select id="findById" resultType="org.javaboy.acls.model.NoticeMessage">
        select * from system_message where id=#{id};
    </select>

    <insert id="save" parameterType="org.javaboy.acls.model.NoticeMessage">
        insert into system_message (id,content) values (#{id},#{content});
    </insert>

    <update id="update" parameterType="org.javaboy.acls.model.NoticeMessage">
        update system_message set content = #{content} where id=#{id};
    </update>
</mapper>
Copy the code

This should all make sense. There’s nothing to say.

Next create NoticeMessageService as follows:

@Service
public class NoticeMessageService {
    @Autowired
    NoticeMessageMapper noticeMessageMapper;

    @PostFilter("hasPermission(filterObject, 'READ')")
    public List<NoticeMessage> findAll(a) {
        List<NoticeMessage> all = noticeMessageMapper.findAll();
        return all;
    }

    @PostAuthorize("hasPermission(returnObject, 'READ')")
    public NoticeMessage findById(Integer id) {
        return noticeMessageMapper.findById(id);
    }

    @PreAuthorize("hasPermission(#noticeMessage, 'CREATE')")
    public NoticeMessage save(NoticeMessage noticeMessage) {
        noticeMessageMapper.save(noticeMessage);
        return noticeMessage;
    }
    
    @PreAuthorize("hasPermission(#noticeMessage, 'WRITE')")
    public void update(NoticeMessage noticeMessage) { noticeMessageMapper.update(noticeMessage); }}Copy the code

Two new notes are involved, just a quick note:

  • PostFilter: returnObject represents the return value of the method after the method is executed to filter out the returned collection or array (for data that the current user has READ permission on). There is a corresponding annotation @prefilter that allows method calls, but the parameters must be filtered before entering the method.
  • PostAuthorize: Allows method calls, but throws a security exception if the expression evaluates to false,#noticeMessageCorresponds to the parameters of the method.
  • @preauthorize: Restricts access to a method based on the calculation results of an expression prior to method invocation.

Once you understand the meaning of the annotations, the above method should not be explained much.

Now that the configuration is complete, let’s test.

4. Test

In order to facilitate the test, we first prepare several test data as follows:

INSERT INTO `acl_class` (`id`, `class`)
VALUES
	(1.'org.javaboy.acls.model.NoticeMessage');
INSERT INTO `acl_sid` (`id`, `principal`, `sid`)
VALUES
	(2.1.'hr'),
	(1.1.'manager'),
	(3.0.'ROLE_EDITOR');
INSERT INTO `system_message` (`id`, `content`)
VALUES
	(1.'111'),
	(2.'222'),
	(3.'333');
Copy the code

Acl_class is first added, then three SIDs are added, two for users and one for a role, and finally three instances of NoticeMessage are added.

Currently, no user/role has access to the three data in system_message. For example, no data is retrieved by executing the following code:

@Test
@WithMockUser(roles = "EDITOR")
public void test01(a) {
    List<NoticeMessage> all = noticeMessageService.findAll();
    System.out.println("all = " + all);
}
Copy the code

@withmockUser (roles = “EDITOR”) indicates access using the EDITOR role. Songo is here for convenience. You can also configure users for Spring Security by yourself, set relevant interfaces, and then add interfaces to Controller for testing, which is not so troublesome here.

Now let’s configure it.

First I want to set hr to be able to read the record with id 1 in system_message as follows:

@Autowired
NoticeMessageService noticeMessageService;
@Autowired
JdbcMutableAclService jdbcMutableAclService;
@Test
@WithMockUser(username = "javaboy")
@Transactional
@Rollback(value = false)
public void test02(a) {
    ObjectIdentity objectIdentity = new ObjectIdentityImpl(NoticeMessage.class, 1);
    Permission p = BasePermission.READ;
    MutableAcl acl = jdbcMutableAclService.createAcl(objectIdentity);
    acl.insertAce(acl.getEntries().size(), p, new PrincipalSid("hr"), true);
    jdbcMutableAclService.updateAcl(acl);
}
Copy the code

The mock user is javaboy. The owner of the MOCK user is Javaboy after the ACL is created, but the Sid in the default data does not have javaboy, so it will automatically add a record to the ACL_SID table. A value of javaboy.

During this process, records are added to the ACL_ENTRY, ACL_OBJect_identity, and ACL_SID tables, so transactions need to be added. Also, since we are executing in a unit test, to ensure that we can see changes to the data in the database, So you need to add the @rollback (value = false) annotation to keep the transaction from rolling back automatically.

Inside the method, you first create the ObjectIdentity and Permission objects, respectively, and then create an ACL object, adding the Javaboy to the ACL_SID table.

The acl_insertAce method is called to store the ACE into the ACL, and the updateAcl method is called to update the ACL object.

After the configuration is complete, execute this method, and the database will have the corresponding record when the execution is complete.

Next, the user hr can read the record with ID 1. As follows:

@Test
@WithMockUser(username = "hr")
public void test03(a) {
    List<NoticeMessage> all = noticeMessageService.findAll();
    assertNotNull(all);
    assertEquals(1, all.size());
    assertEquals(1, all.get(0).getId());
    NoticeMessage byId = noticeMessageService.findById(1);
    assertNotNull(byId);
    assertEquals(1, byId.getId());
}
Copy the code

There are two methods that Songo uses here to show you. First, we called findAll. This method will findAll the data, and then the return result will be automatically filtered, leaving only the data that hr user has access to, i.e. the data with id 1. The other call is the findById method, passing in an argument of 1, which makes sense.

If you want to modify the object using the hr user at this point, you cannot. We can continue with the above code and allow hr to modify the record with id 1 as follows:

@Test
@WithMockUser(username = "javaboy")
@Transactional
@Rollback(value = false)
public void test02(a) {
    ObjectIdentity objectIdentity = new ObjectIdentityImpl(NoticeMessage.class, 1);
    Permission p = BasePermission.WRITE;
    MutableAcl acl = (MutableAcl) jdbcMutableAclService.readAclById(objectIdentity);
    acl.insertAce(acl.getEntries().size(), p, new PrincipalSid("hr"), true);
    jdbcMutableAclService.updateAcl(acl);
}
Copy the code

Note that the permission is changed to WRITE. Because this ObjectIdentity already exists in the ACL, the readAclById method is used to read the existing ACL. After executing the hr user write permission method, we will test the hr user write permission again:

@Test
@WithMockUser(username = "hr")
public void test04(a) {
    NoticeMessage msg = noticeMessageService.findById(1);
    assertNotNull(msg);
    assertEquals(1, msg.getId());
    msg.setContent("javaboy-1111");
    noticeMessageService.update(msg);
    msg = noticeMessageService.findById(1);
    assertNotNull(msg);
    assertEquals("javaboy-1111", msg.getContent());
}
Copy the code

At this point, HR can use WRITE permission to modify the object.

Suppose I now want the manager user to create a NoticeMessage with ID 99. By default, the Manager does not have this privilege. We can now empower him:

@Test
@WithMockUser(username = "javaboy")
@Transactional
@Rollback(value = false)
public void test02(a) {
    ObjectIdentity objectIdentity = new ObjectIdentityImpl(NoticeMessage.class, 99);
    Permission p = BasePermission.CREATE;
    MutableAcl acl = jdbcMutableAclService.createAcl(objectIdentity);
    acl.insertAce(acl.getEntries().size(), p, new PrincipalSid("manager"), true);
    jdbcMutableAclService.updateAcl(acl);
}
Copy the code

Notice that the permission here is CREATE.

Next, use the Manager user to add data:

@Test
@WithMockUser(username = "manager")
public void test05(a) {
    NoticeMessage noticeMessage = new NoticeMessage();
    noticeMessage.setId(99);
    noticeMessage.setContent("999");
    noticeMessageService.save(noticeMessage);
}
Copy the code

At this point, you can add successfully. After the data is added successfully, the manager user does not have the permission to read the data whose ID is 99. You can add the data by referring to the previous example.

5. Summary

As you can see from the above example, the permission control in the ACL permission model is really very, very detailed, down to the CURD of each object.

Advantages go without saying, enough fine! At the same time, services and rights are separated. The disadvantages are also obvious, the amount of authority data is huge, the scalability is weak.

Finally, the public number background reply ACL to obtain this case download link.