領域物件安全性 (ACLs)

本節介紹 Spring Security 如何透過訪問控制列表 (ACL) 提供領域物件安全性。

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

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

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

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

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

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

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

關鍵概念

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

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” 代表 “Security IDentity”)。唯一的列是 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 儲存分配給每個接收者的 individual permissions。列包括指向 ACL_OBJECT_IDENTITY 的外部索引鍵、接收者(即指向 ACL_SID 的外部索引鍵)、是否進行審計以及表示授予或拒絕的實際許可權的整數位掩碼。每個獲得與領域物件互動許可權的接收者對應一行。

正如上一段所述,ACL 系統使用整數位掩碼。然而,您無需瞭解位移的細節即可使用 ACL 系統。可以說,我們有 32 位可以開啟或關閉。每個位代表一個許可權。預設情況下,許可權包括讀 (bit 0)、寫 (bit 1)、建立 (bit 2)、刪除 (bit 3) 和管理 (bit 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” 是 “Security IDentity” 的縮寫)。常見的類包括 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 模組。第一個是聯絡人示例 (Contacts Sample),另一個是文件管理系統 (DMS) 示例 (Document Management System (DMS) Sample)。我們建議檢視這些示例。

入門

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

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

以下程式碼片段展示瞭如何建立或修改現有的 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)

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

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

一旦您使用這裡描述的技術在資料庫中儲存了一些 ACL 資訊,下一步就是實際使用 ACL 資訊作為授權決策邏輯的一部分。您這裡有多種選擇。您可以編寫自己的 AccessDecisionVoterAfterInvocationProvider,它們分別在方法呼叫之前或之後觸發。這些類將使用 AclService 檢索相關的 ACL,然後呼叫 Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode) 來決定是否授予或拒絕許可權。或者,您可以使用我們的 AclEntryVoterAclEntryAfterInvocationProviderAclEntryAfterInvocationCollectionFilteringProvider 類。所有這些類都提供了基於宣告性方法在執行時評估 ACL 資訊,使您無需編寫任何程式碼。

請參閱示例應用程式以瞭解如何使用這些類。