一個案例演示 Spring Security 中粒度超細的權限控制!

想要細化權限控制粒度,辦法不少。本文接着上文(Spring Security 中如何細化權限粒度?),經過一個具體的案例來向小夥伴們展現基於 Acl 的權限控制。其餘的權限控制模型後面也會一一介紹。java

1.準備工做

首先建立一個 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

至此,準備工做就算完成了。接下來咱們來看配置。安全

2.ACL 配置

這塊配置代碼量比較大,我先把代碼擺上來,咱們再逐個分析: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;
    }
}
  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 獲取本文案例下載連接。

相關文章
相關標籤/搜索