網域物件安全 (ACLs)

本節說明 Spring Security 如何透過存取控制清單 (ACL) 提供網域物件安全。

複雜的應用程式通常需要定義超出 Web 請求或方法調用層級的存取權限。相反地,安全性決策需要包含「誰」(身份驗證)、「在哪裡」(方法調用) 和「什麼」(某個網域物件)。換句話說,授權決策也需要考量方法調用的實際網域物件實例。

假設您正在為寵物診所設計應用程式。您的 Spring 應用程式主要有兩大類使用者:寵物診所的員工和寵物診所的顧客。員工應能存取所有資料,而顧客應僅能看到自己的顧客記錄。為了增加趣味性,您的顧客可以讓其他使用者查看他們的顧客記錄,例如他們的「幼犬幼兒園」導師或當地「小馬俱樂部」的主席。當您使用 Spring Security 作為基礎時,您有幾種可能的方法:

  • 編寫您的業務方法以強制執行安全性。您可以查閱 Customer 網域物件實例中的集合,以判斷哪些使用者具有存取權。透過使用 SecurityContextHolder.getContext().getAuthentication(),您可以存取 Authentication 物件。

  • 編寫一個 AccessDecisionVoter,以從儲存在 Authentication 物件中的 GrantedAuthority[] 實例強制執行安全性。這表示您的 AuthenticationManager 需要使用自訂的 GrantedAuthority[] 物件來填充 Authentication,以代表主體可以存取的每個 Customer 網域物件實例。

  • 編寫一個 AccessDecisionVoter,以強制執行安全性並直接開啟目標 Customer 網域物件。這表示您的 voter 需要存取 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 權限的每個唯一 Class 都有一列。

  • 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” 是 “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,另一個是 Document Management System (DMS) Sample。我們建議您查看這些範例。

開始使用

若要開始使用 Spring Security 的 ACL 功能,您需要將 ACL 資訊儲存在某個地方。這需要實例化 Spring 中的 DataSource。然後,DataSource 會注入到 JdbcMutableAclServiceBasicLookupStrategy 實例中。前者提供變更器功能,後者提供高效能的 ACL 檢索功能。請參閱 Spring Security 隨附的 範例 之一,以取得組態範例。您還需要使用上一節中列出的四個 ACL 特定資料表來填充資料庫(請參閱 ACL 範例以取得適當的 SQL 陳述式)。

一旦您建立了所需的結構描述並實例化 JdbcMutableAclService,您需要確保您的網域模型支援與 Spring Security ACL 套件的互操作性。希望 ObjectIdentityImpl 足以滿足需求,因為它提供了許多可以使用它的方式。大多數人的網域物件都包含一個 public Serializable getId() 方法。如果傳回類型為 long 或與 long 相容(例如 int),您可能會發現您無需進一步考慮 ObjectIdentity 問題。ACL 模組的許多部分都依賴 long 識別符。如果您不使用 long(或 intbyte 等),您可能需要重新實作許多類別。我們不打算在 Spring Security 的 ACL 模組中支援非 long 識別符,因為 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 不提供任何特殊整合,以在您的 DAO 或儲存庫操作中自動建立、更新或刪除 ACL。相反地,您需要為您的個別網域物件編寫類似於前面範例中所示的程式碼。您應該考慮在您的服務層上使用 AOP,以自動將 ACL 資訊與您的服務層操作整合。我們發現這種方法是有效的。

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

請參閱範例應用程式以了解如何使用這些類別。