域物件安全 (ACLs)

本節描述 Spring Security 如何使用訪問控制列表 (ACL) 提供域物件安全。

複雜的應用程式通常需要定義超出 Web 請求或方法呼叫級別的訪問許可權。相反,安全決策需要包括誰 (Authentication)、何處 (MethodInvocation) 和什麼 (SomeDomainObject)。換句話說,授權決策還需要考慮方法呼叫所涉及的實際域物件例項。

想象一下,您正在為一家寵物診所設計一個應用程式。您的 Spring 應用程式有兩個主要使用者組:寵物診所的工作人員和寵物診所的客戶。工作人員應該能夠訪問所有資料,而客戶應該只能檢視他們自己的客戶記錄。為了讓事情變得更有趣,您的客戶可以允許其他使用者檢視他們的客戶記錄,例如他們的“幼犬學前班”導師或他們當地“小馬俱樂部”的主席。當您使用 Spring Security 作為基礎時,您有幾種可能的方法

  • 編寫業務方法以強制執行安全性。您可以在 Customer 域物件例項中查詢一個集合,以確定哪些使用者具有訪問許可權。透過使用 SecurityContextHolder.getContext().getAuthentication(),您可以訪問 Authentication 物件。

  • 編寫一個 AuthorizationManager 以從儲存在 Authentication 物件中的 GrantedAuthority[] 例項強制執行安全性。這意味著您的 AuthenticationManager 需要使用自定義的 GrantedAuthority[] 物件來填充 Authentication,以表示主體可以訪問的每個 Customer 域物件例項。

  • 編寫一個 AuthorizationManager 以強制執行安全性並直接開啟目標 Customer 域物件。這意味著您的投票者需要訪問一個 DAO,該 DAO 允許它檢索 Customer 物件。然後,它可以訪問 Customer 物件的批准使用者集合並做出適當的決策。

這些方法中的每一種都完全合法。然而,第一種方法將您的授權檢查與您的業務程式碼耦合。這帶來的主要問題包括單元測試難度增加,以及 Customer 授權邏輯在其他地方重用更加困難。從 Authentication 物件獲取 GrantedAuthority[] 例項也很好,但對於大量 Customer 物件來說無法擴充套件。如果一個使用者可以訪問 5,000 個 Customer 物件(在這種情況下不太可能,但想象一下如果它是一個大型小馬俱樂部的熱門獸醫!),消耗的記憶體量和構建 Authentication 物件所需的時間將是不可取的。最後一種方法,直接從外部程式碼開啟 Customer,可能是這三種方法中最好的。它實現了關注點分離,並且不會濫用記憶體或 CPU 週期,但它仍然效率低下,因為 AuthorizationManager 和最終的業務方法本身都執行對負責檢索 Customer 物件的 DAO 的呼叫。每次方法呼叫兩次訪問顯然是不可取的。此外,對於列出的每種方法,您都需要從頭開始編寫自己的訪問控制列表 (ACL) 持久化和業務邏輯。

幸運的是,還有另一種替代方案,我們將在後面討論。

核心概念

Spring Security 的 ACL 服務打包在 spring-security-acl-xxx.jar 中。您需要將此 JAR 新增到您的類路徑中才能使用 Spring Security 的域物件例項安全功能。

如果您需要訪問包含 AclEntryVoter 的舊版訪問 API,請同時包含 spring-security-access-xxx.jar

Spring Security 的域物件例項安全功能圍繞訪問控制列表 (ACL) 的概念展開。系統中的每個域物件例項都有自己的 ACL,ACL 記錄了誰可以和不能處理該域物件的詳細資訊。考慮到這一點,Spring Security 為您的應用程式提供了三個主要的 ACL 相關功能

  • 一種有效檢索所有域物件的 ACL 條目(並修改這些 ACL)的方法

  • 一種在方法呼叫前確保給定主體被允許處理您的物件的方法

  • 一種在方法呼叫後確保給定主體被允許處理您的物件(或它們返回的內容)的方法

如第一點所述,Spring Security ACL 模組的主要功能之一是提供一種高效能的 ACL 檢索方式。這種 ACL 儲存庫功能極其重要,因為系統中的每個域物件例項都可能有多個訪問控制條目,並且每個 ACL 都可能以樹狀結構繼承自其他 ACL(Spring Security 支援這一點,並且非常常用)。Spring Security 的 ACL 功能經過精心設計,可提供高效能的 ACL 檢索,並支援可插拔快取、最小化死鎖的資料庫更新、獨立於 ORM 框架(我們直接使用 JDBC)、適當的封裝和透明的資料庫更新。

鑑於資料庫是 ACL 模組執行的核心,我們需要探索預設情況下在實現中使用的四張主要表。這些表按典型的 Spring Security ACL 部署中的大小順序呈現,行數最多的表列在最後

  • ACL_SID 允許我們唯一標識系統中的任何主體或許可權(“SID”代表“安全身份”)。唯一的列是 ID、SID 的文字表示以及一個標誌,指示文字表示是指主體名稱還是 GrantedAuthority。因此,每個唯一的主體或 GrantedAuthority 都有一個單獨的行。在接收許可權的上下文中,SID 通常被稱為“接收者”。

  • ACL_CLASS 允許我們唯一標識系統中的任何域物件類。唯一的列是 ID 和 Java 類名。因此,對於我們希望儲存 ACL 許可權的每個唯一類,都有一行。

  • ACL_OBJECT_IDENTITY 儲存系統中每個唯一域物件例項的資訊。列包括 ID、指向 ACL_CLASS 表的外部索引鍵、一個唯一識別符號(以便我們知道我們提供資訊的 ACL_CLASS 例項)、父級、指向 ACL_SID 表的外部索引鍵(用於表示域物件例項的所有者),以及我們是否允許 ACL 條目從任何父 ACL 繼承。對於我們儲存 ACL 許可權的每個域物件例項,我們都有一行。

  • 最後,ACL_ENTRY 儲存分配給每個接收者的單個許可權。列包括指向 ACL_OBJECT_IDENTITY 的外部索引鍵、接收者(即指向 ACL_SID 的外部索引鍵)、我們是否進行審計以及表示實際授予或拒絕的許可權的整數位掩碼。對於每個獲得處理域物件許可權的接收者,我們都有一行。

如上段所述,ACL 系統使用整數位掩碼。但是,您無需瞭解位移的精細點即可使用 ACL 系統。只需說我們可以開啟或關閉 32 位。每個位都代表一個許可權。預設情況下,許可權是讀取(位 0)、寫入(位 1)、建立(位 2)、刪除(位 3)和管理(位 4)。如果您希望使用其他許可權,可以實現自己的 Permission 例項,而 ACL 框架的其餘部分在不瞭解您的擴充套件的情況下執行。

您應該明白,系統中域物件的數量與我們選擇使用整數位掩碼的事實絕對無關。雖然您有 32 位許可權可用,但您可以擁有數十億個域物件例項(這意味著 ACL_OBJECT_IDENTITY 中有數十億行,可能還有 ACL_ENTRY)。我們指出這一點是因為我們發現人們有時會錯誤地認為他們需要為每個潛在域物件分配一個位,但事實並非如此。

現在我們已經對 ACL 系統做了什麼以及它在表結構級別上是什麼樣子進行了基本概述,我們需要探索關鍵介面

  • Acl:每個域物件都有且只有一個 Acl 物件,它內部包含 AccessControlEntry 物件並知道 Acl 的所有者。Acl 不直接引用域物件,而是引用 ObjectIdentityAcl 儲存在 ACL_OBJECT_IDENTITY 表中。

  • AccessControlEntry:一個 Acl 包含多個 AccessControlEntry 物件,在框架中通常縮寫為 ACE。每個 ACE 都指向一個特定的 PermissionSidAcl 元組。ACE 還可以是授予或非授予的,幷包含審計設定。ACE 儲存在 ACL_ENTRY 表中。

  • Permission:許可權表示一個特定的不可變位掩碼,並提供位掩碼和輸出資訊的便利函式。上面介紹的基本許可權(位 0 到 4)包含在 BasePermission 類中。

  • Sid:ACL 模組需要引用主體和 GrantedAuthority[] 例項。Sid 介面提供了一層間接性。(“SID”是“安全身份”的縮寫。)常見類包括 PrincipalSid(表示 Authentication 物件中的主體)和 GrantedAuthoritySid。安全身份資訊儲存在 ACL_SID 表中。

  • ObjectIdentity:每個域物件在 ACL 模組內部都由一個 ObjectIdentity 表示。預設實現稱為 ObjectIdentityImpl

  • AclService:檢索適用於給定 ObjectIdentityAcl。在包含的實現 (JdbcAclService) 中,檢索操作委託給 LookupStrategyLookupStrategy 提供了一種高度最佳化的策略來檢索 ACL 資訊,使用批次檢索 (BasicLookupStrategy) 並支援使用物化檢視、分層查詢和類似以效能為中心、非 ANSI SQL 功能的自定義實現。

  • MutableAclService:允許將修改後的 Acl 呈現進行持久化。此介面的使用是可選的。

請注意,我們的 AclService 和相關的資料庫類都使用 ANSI SQL。因此,這應該適用於所有主要資料庫。在撰寫本文時,該系統已成功透過 Hypersonic SQL、PostgreSQL、Microsoft SQL Server 和 Oracle 的測試。

Spring Security 附帶了兩個示例,演示了 ACL 模組。第一個是 聯絡人示例,另一個是 文件管理系統 (DMS) 示例。我們建議您檢視這些示例。

入門

要開始使用 Spring Security 的 ACL 功能,您需要將 ACL 資訊儲存在某個地方。這需要在 Spring 中例項化一個 DataSource。然後將 DataSource 注入到 JdbcMutableAclServiceBasicLookupStrategy 例項中。前者提供修改器功能,後者提供高效能 ACL 檢索功能。有關示例配置,請參閱 Spring Security 附帶的 示例 之一。您還需要使用上一節中列出的 四個 ACL 特定表 填充資料庫(有關適當的 SQL 語句,請參閱 ACL 示例)。

建立所需的模式並例項化 JdbcMutableAclService 後,您需要確保您的域模型支援與 Spring Security ACL 包的互操作性。希望 ObjectIdentityImpl 足夠,因為它提供了多種使用方式。大多數人都有包含 public Serializable getId() 方法的域物件。如果返回型別是 long 或與 long 相容(例如 int),您可能會發現無需進一步考慮 ObjectIdentity 問題。ACL 模組的許多部分都依賴於長識別符號。如果您不使用 long(或 intbyte 等),您可能需要重新實現許多類。我們不打算在 Spring Security 的 ACL 模組中支援非長識別符號,因為 long 已經與所有資料庫序列相容,是最常見的識別符號資料型別,並且長度足以適應所有常見使用場景。

以下程式碼片段展示瞭如何建立 Acl 或修改現有 Acl

  • Java

  • Kotlin

// Prepare the information we'd like in our access control entry (ACE)
ObjectIdentity oi = new ObjectIdentityImpl(Foo.class, new Long(44));
Sid sid = new PrincipalSid("Samantha");
Permission p = BasePermission.ADMINISTRATION;

// Create or update the relevant ACL
MutableAcl acl = null;
try {
acl = (MutableAcl) aclService.readAclById(oi);
} catch (NotFoundException nfe) {
acl = aclService.createAcl(oi);
}

// Now grant some permissions via an access control entry (ACE)
acl.insertAce(acl.getEntries().length, p, sid, true);
aclService.updateAcl(acl);
val oi: ObjectIdentity = ObjectIdentityImpl(Foo::class.java, 44)
val sid: Sid = PrincipalSid("Samantha")
val p: Permission = BasePermission.ADMINISTRATION

// Create or update the relevant ACL
var acl: MutableAcl? = null
acl = try {
aclService.readAclById(oi) as MutableAcl
} catch (nfe: NotFoundException) {
aclService.createAcl(oi)
}

// Now grant some permissions via an access control entry (ACE)
acl!!.insertAce(acl.entries.size, p, sid, true)
aclService.updateAcl(acl)

在前面的示例中,我們檢索了與識別符號為 44 的 Foo 域物件關聯的 ACL。然後,我們添加了一個 ACE,以便名為“Samantha”的主體可以“管理”該物件。除了 insertAce 方法之外,該程式碼片段相對不言自明。insertAce 方法的第一個引數確定新條目插入到 Acl 中的位置。在前面的示例中,我們將新的 ACE 放在現有 ACE 的末尾。最後一個引數是一個布林值,指示 ACE 是授予還是拒絕。大多數情況下是授予 (true)。但是,如果它拒絕 (false),則許可權實際上被阻止。

Spring Security 不提供任何特殊整合來自動建立、更新或刪除 ACL 作為您的 DAO 或儲存庫操作的一部分。相反,您需要為您的單個域物件編寫類似於前面示例中所示的程式碼。您應該考慮在您的服務層上使用 AOP 來自動將 ACL 資訊與您的服務層操作整合。我們發現這種方法是有效的。

使用 PermissionEvaluator

一旦您使用此處描述的技術將一些 ACL 資訊儲存在資料庫中,下一步就是實際將 ACL 資訊用作授權決策邏輯的一部分。

您在此處有多種選擇,主要是在您的 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 註解表示式中使用 AclPermissionEvaluator

以下是將 AclPersmissionEvaluator 引入您的授權邏輯所需的元件示例

  • Java

  • Kotlin

@EnableMethodSecurity
@Configuration
class SecurityConfig {
	@Bean
	static MethodSecurityExpressionHandler expressionHandler(AclPermissionEvaluator aclPermissionEvaluator) {
		final DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
		expressionHandler.setPermissionEvaluator(aclPermissionEvaluator);
		return expressionHandler;
	}

	@Bean
	static AclPermissionEvaluator aclPermissionEvaluator(AclService aclService) {
		return new AclPermissionEvaluator(aclService);
	}

	@Bean
	static JdbcMutableAclService aclService(DataSource dataSource, LookupStrategy lookupStrategy, AclCache aclCache) {
		return new JdbcMutableAclService(dataSource, lookupStrategy, aclCache);
	}

	@Bean
	static LookupStrategy lookupStrategy(DataSource dataSource, AclCache cache,
			AclAuthorizationStrategy aclAuthorizationStrategy, PermissionGrantingStrategy permissionGrantingStrategy) {
		return new BasicLookupStrategy(dataSource, cache, aclAuthorizationStrategy, permissionGrantingStrategy);
	}

	@Bean
	static AclCache aclCache(PermissionGrantingStrategy permissionGrantingStrategy,
			AclAuthorizationStrategy aclAuthorizationStrategy) {
		Cache cache = new ConcurrentMapCache("aclCache");
		return new SpringCacheBasedAclCache(cache, permissionGrantingStrategy, aclAuthorizationStrategy);
	}

	@Bean
	static AclAuthorizationStrategy aclAuthorizationStrategy() {
		return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ADMIN"));
	}

	@Bean
	static PermissionGrantingStrategy permissionGrantingStrategy() {
		return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
	}
}
@EnableMethodSecurity
@Configuration
internal object SecurityConfig {
    @Bean
    fun expressionHandler(aclPermissionEvaluator: AclPermissionEvaluator?): MethodSecurityExpressionHandler {
        val expressionHandler = DefaultMethodSecurityExpressionHandler()
        expressionHandler.setPermissionEvaluator(aclPermissionEvaluator)
        return expressionHandler
    }

    @Bean
    fun aclPermissionEvaluator(aclService: AclService?): AclPermissionEvaluator {
        return AclPermissionEvaluator(aclService)
    }

    @Bean
    fun aclService(dataSource: DataSource?, lookupStrategy: LookupStrategy?, aclCache: AclCache?): JdbcMutableAclService {
        return JdbcMutableAclService(dataSource, lookupStrategy, aclCache)
    }

    @Bean
    fun lookupStrategy(dataSource: DataSource?, cache: AclCache?,
    aclAuthorizationStrategy: AclAuthorizationStrategy?, permissionGrantingStrategy: PermissionGrantingStrategy?): LookupStrategy {
        return BasicLookupStrategy(dataSource, cache, aclAuthorizationStrategy, permissionGrantingStrategy)
    }

    @Bean
    fun aclCache(permissionGrantingStrategy: PermissionGrantingStrategy?,
    aclAuthorizationStrategy: AclAuthorizationStrategy?): AclCache {
        val cache: Cache = ConcurrentMapCache("aclCache")
        return SpringCacheBasedAclCache(cache, permissionGrantingStrategy, aclAuthorizationStrategy)
    }

    @Bean
    fun aclAuthorizationStrategy(): AclAuthorizationStrategy {
        return AclAuthorizationStrategyImpl(SimpleGrantedAuthority("ADMIN"))
    }

    @Bean
    fun permissionGrantingStrategy(): PermissionGrantingStrategy {
        return DefaultPermissionGrantingStrategy(ConsoleAuditLogger())
    }
}

然後使用 基於方法安全,您可以在註解表示式中使用 hasPermission,如下所示

  • Java

  • Kotlin

@GetMapping
@PostFilter("hasPermission(filterObject, read)")
Iterable<Message> getAll() {
	return this.messagesRepository.findAll();
}
@GetMapping
@PostFilter("hasPermission(filterObject, read)")
fun getAll(): Iterable<Message> {
    return this.messagesRepository.findAll()
}
© . This site is unofficial and not affiliated with VMware.