想要细化权限控制粒度,办法不少。本文接着上文(Spring Security 中如何细化权限粒度?),经过一个具体的案例来向小伙伴们展现基于 Acl 的权限控制。其余的权限控制模型后面也会一一介绍。java
首先建立一个 Spring Boot 项目,因为咱们这里涉及到数据库操做,因此除了 Spring Security 依赖以外,还须要加入数据库驱动以及 MyBatis 依赖。mysql
因为没有 acl 相关的 starter,因此须要咱们手动添加 acl 依赖,另外 acl 还依赖于 ehcache 缓存,因此还须要加上缓存依赖。web
最终的 pom.xml 文件以下:spring
<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 包中能够找到数据库脚本文件:sql
根据本身的数据库选择合适的脚本执行,执行后一共建立了四张表,以下:数据库
表的含义我就不作过多解释了,不清楚的小伙伴能够参考上篇文章: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
至此,准备工做就算完成了。接下来咱们来看配置。安全
这块配置代码量比较大,我先把代码摆上来,咱们再逐个分析:mybatis
@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; } }
@PreAuthorize("hasPermission(#noticeMessage, 'WRITE')")
这样的注解进行权限控制,所以之类须要配置一个 PermissionEvaluator 实例。至此,这里的配置类就和你们介绍完了。
假设我如今有一个通知消息类 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); } }
涉及到了两个新注解,稍微说下:
#noticeMessage
对应了方法的参数。明白了注解的含义,那么上面的方法应该就不用多作解释了吧。
配置完成,接下来咱们进行测试。
为了方便测试,咱们首先准备几条测试数据,以下:
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 的数据的权限,能够参考前面案例自行添加。
从上面的案例中你们能够看到,ACL 权限模型中的权限控制真的是很是很是细,细到每个对象的 CURD。
优势就不用说了,够细!同时将业务和权限成功分离。缺点也很明显,权限数据量庞大,扩展性弱。
最后,公号后台回复 acl 获取本文案例下载连接。