域对象安全 (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.