乐趣区

关于java:一个案例演示-Spring-Security-中粒度超细的权限控制

想要细化权限管制粒度,方法很多。本文接着上文(Spring Security 中如何细化权限粒度?),通过一个具体的案例来向小伙伴们展现基于 Acl 的权限管制。其余的权限管制模型前面也会一一介绍。

1. 筹备工作

首先创立一个 Spring Boot 我的项目,因为咱们这里波及到数据库操作,所以除了 Spring Security 依赖之外,还须要退出数据库驱动以及 MyBatis 依赖。

因为没有 acl 相干的 starter,所以须要咱们手动增加 acl 依赖,另外 acl 还依赖于 ehcache 缓存,所以还须要加上缓存依赖。

最终的 pom.xml 文件如下:

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

我的项目创立胜利之后,咱们在 acl 的 jar 包中能够找到数据库脚本文件:

依据本人的数据库抉择适合的脚本执行,执行后一共创立了四张表,如下:

表的含意我就不做过多解释了,不分明的小伙伴能够参考上篇文章:Spring Security 中如何细化权限粒度?

最初,再在我的项目的 application.properties 文件中配置数据库信息,如下:

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

至此,筹备工作就算实现了。接下来咱们来看配置。

2.ACL 配置

这块配置代码量比拟大,我先把代码摆上来,咱们再一一剖析:

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

    @Autowired
    DataSource dataSource;

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

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

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

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

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

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

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

    @Bean
    PermissionEvaluator permissionEvaluator() {AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService());
        return permissionEvaluator;
    }
}
  1. @EnableGlobalMethodSecurity 注解的配置示意开启我的项目中 @PreAuthorize、@PostAuthorize 以及 @Secured 注解的应用,一会咱们要通过这些注解配置权限。
  2. 因为引入了数据库的一整套货色,并且配置了数据库连贯信息,所以这里能够注入 DataSource 实例以备后续应用。
  3. AclAuthorizationStrategy 实例用来判断以后的认证主体是否有批改 Acl 的权限,精确来说是三种权限:批改 Acl 的 owner;批改 Acl 的审计信息以及批改 ACE 自身。这个接口只有一个实现类就是 AclAuthorizationStrategyImpl,咱们在创立实例时,能够传入三个参数,别离对应了这三种权限,也能够传入一个参数,示意这一个角色能够干三件事。
  4. PermissionGrantingStrategy 接口提供了一个 isGranted 办法,这个办法就是最终真正进行权限比对的办法,该接口只有一个实现类 DefaultPermissionGrantingStrategy,间接 new 就行了。
  5. 在 ACL 体系中,因为权限比对总是要查询数据库,造成了性能问题,因而引入了 Ehcache 做缓存。AclCache 共有两个实现类:SpringCacheBasedAclCache 和 EhCacheBasedAclCache。咱们后面曾经引入了 ehcache 实例,所以这里配置 EhCacheBasedAclCache 实例即可。
  6. LookupStrategy 能够通过 ObjectIdentity 解析出对应的 Acl。LookupStrategy 只有一个实现类就是 BasicLookupStrategy,间接 new 即可。
  7. AclService 这个咱们在上文曾经介绍过了,这里不再赘述。
  8. PermissionEvaluator 是为表达式 hasPermission 提供反对的。因为本案例前面应用相似于 @PreAuthorize("hasPermission(#noticeMessage,'WRITE')") 这样的注解进行权限管制,因而之类须要配置一个 PermissionEvaluator 实例。

至此,这里的配置类就和大家介绍完了。

3. 情节设定

假如我当初有一个告诉音讯类 NoticeMessage,如下:

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

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

    public Integer getId() {return id;}

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

    public String getContent() {return content;}

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

而后依据该类创立了数据表:

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;

那么接下来的权限管制就是针对这个 NoticeMessage 的。

创立 NoticeMessageMapper,并增加几个测试方法:

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

    NoticeMessage findById(Integer id);

    void save(NoticeMessage noticeMessage);

    void update(NoticeMessage noticeMessage);
}

NoticeMessageMapper.xml 内容如下:

<!DOCTYPE mapper
        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>

这些应该都好了解,没啥好说的。

接下来创立 NoticeMessageService,如下:

@Service
public class NoticeMessageService {
    @Autowired
    NoticeMessageMapper noticeMessageMapper;

    @PostFilter("hasPermission(filterObject,'READ')")
    public List<NoticeMessage> findAll() {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);
    }

}

波及到了两个新注解,略微说下:

  • @PostFilter:在执行办法后过滤返回的汇合或数组(筛选出以后用户具备 READ 权限的数据),returnObject 就示意办法的返回值。有一个和它对应的注解 @PreFilter,这个注解容许办法调用,但必须在进入办法之前对参数进行过滤。
  • @PostAuthorize:容许办法调用,然而如果表达式计算结果为 false,将抛出一个安全性异样,#noticeMessage 对应了办法的参数。
  • @PreAuthorize:在办法调用之前,基于表达式的计算结果来限度对办法的拜访。

明确了注解的含意,那么下面的办法应该就不必多做解释了吧。

配置实现,接下来咱们进行测试。

4. 测试

为了不便测试,咱们首先筹备几条测试数据,如下:

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');

首先增加了 acl_class,而后增加了三个 Sid,两个是用户,一个是角色,最初增加了三个 NoticeMessage 实例。

目前没有任何用户 / 角色可能拜访到 system_message 中的三条数据。例如执行如下代码获取不到任何数据:

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

@WithMockUser(roles = “EDITOR”) 示意应用 EDITOR 角色拜访。松哥这里是为了不便。小伙伴们也能够本人给 Spring Security 配置用户,设置相干接口,而后 Controller 中增加接口进行测试,我这里就不那么麻烦了。

当初咱们对其进行配置。

首先我想设置让 hr 这个用户能够读取 system_message 表中 id 为 1 的记录,形式如下:

@Autowired
NoticeMessageService noticeMessageService;
@Autowired
JdbcMutableAclService jdbcMutableAclService;
@Test
@WithMockUser(username = "javaboy")
@Transactional
@Rollback(value = false)
public void test02() {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);
}

咱们设置了 mock user 是 javaboy,也就是这个 acl 创立好之后,它的 owner 是 javaboy,然而咱们后面预设数据中 Sid 没有 javaboy,所以会主动向 acl_sid 表中增加一条记录,值为 javaboy。

在这个过程中,会别离向 acl_entry、acl_object_identity 以及 acl_sid 三张表中增加记录,因而须要增加事务,同时因为咱们是在单元测试中执行,为了确保可能看到数据库中数据的变动,所以须要增加 @Rollback(value = false) 注解让事务不要主动回滚。

在办法外部,首先别离创立 ObjectIdentity 和 Permission 对象,而后创立一个 acl 对象进去,这个过程中会将 javaboy 增加到 acl_sid 表中。

接下来调用 acl_insertAce 办法,将 ace 存入 acl 中,最初调用 updateAcl 办法去更新 acl 对象即可。

配置实现后,执行该办法,执行实现后,数据库中就会有相应的记录了。

接下来,应用 hr 这个用户就能够读取到 id 为 1 的记录了。如下:

@Test
@WithMockUser(username = "hr")
public void test03() {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());
}

松哥这里用了两个办法来和大家演示。首先咱们调用了 findAll,这个办法会查问出所有的数据,而后返回后果会被主动过滤,只剩下 hr 用户具备读取权限的数据,即 id 为 1 的数据;另一个调用的就是 findById 办法,传入参数为 1,这个好了解。

如果此时想利用 hr 这个用户批改对象,则是不能够的。咱们能够持续应用下面的代码,让 hr 这个用户能够批改 id 为 1 的记录,如下:

@Test
@WithMockUser(username = "javaboy")
@Transactional
@Rollback(value = false)
public void test02() {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);
}

留神这里权限改为 WRITE 权限。因为 acl 中曾经存在这个 ObjectIdentity 了,所以这里通过 readAclById 办法间接读取已有的 acl 即可。办法执行结束后,咱们再进行 hr 用户写权限的测试:

@Test
@WithMockUser(username = "hr")
public void test04() {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());
}

此时,hr 就能够应用 WRITE 权限去批改对象了。

假如我当初想让 manager 这个用户去创立一个 id 为 99 的 NoticeMessage,默认状况下,manager 是没有这个权限的。咱们当初能够给他赋权:

@Test
@WithMockUser(username = "javaboy")
@Transactional
@Rollback(value = false)
public void test02() {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);
}

留神,这里的权限是 CREATE。

接下来应用 manager 用户就能够增加数据了:

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

此时就能够增加胜利了。增加胜利后,manager 这个用户没有读 id 为 99 的数据的权限,能够参考后面案例自行添加。

5. 小结

从下面的案例中大家能够看到,ACL 权限模型中的权限管制真的是十分十分细,细到每一个对象的 CURD。

长处就不用说了,够细!同时将业务和权限胜利拆散。毛病也很显著,权限数据量宏大,扩展性弱。

最初,公号后盾回复 acl 获取本文案例下载链接。

退出移动版