想要細化權限控制粒度,辦法不少。本文接着上文(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 獲取本文案例下載連接。