域对象安全

1. 前言

相比 Web 请求的安全及方法调用级别的安全,有些应用还会定义更加复杂的访问权限。在这种情况下,权限策略需要同时包含:

  • who」通过认证(Authentication)完成;
  • where」在什么地方应用;
  • what」安全对象是什么。

也就是说,权限策略除了考虑调用的方法,还有考虑调用域对象的实例。

举例说明。假设我们设计一个宠物诊所的管理系统,该系统有两个主要用户组:工作人员和客户。员工可以访问所有动物数据,而客户只能查看自己的数据。假设我们为该系统扩展了新的功能,即客户可以授权其他用户查看自己的数据,比如其他宠物医院的关联机构,宠物俱乐部等等。在 Spring Security 项目中,我们有几种实现方法:

  • 在业务方法中实现安全策略。

    比如,我们可以在「客户」的域对象实例中放置集合,通过权限的配置内容,判断集合中哪个用户拥有访问权限。这种方式下,我们使用 SecurityContextHolder.getContext().getAuthentication() 方式获取权限对象。

  • 自定义访问决策实例 AccessDecisionVoter 并配和 GrantedAuthority 对象。

    扩展实现 AccessDecisionVoter ,通过 Authentication 对象中 GrantedAuthority[] 集合的内容实现安全策略。这种方式下,我们需要在权限对象 GrantedAuthority 体现出该主体是否有对其他「客户」的访问权限。

  • 自定义访问决策实例 AccessDecisionVoter 直接通过「客户」域对象实例判定「客户」权限。

    这种情况下 AccessDecisionVoter 对象需要有检索「客户」的数据访问接口。

上述的方法都是适用的。但是,第一种方式中,授权检查的代码将会和业务代码紧密关联,耦合度高,不便于单元测试。适用 GrantedAuthority 的方式的缺点是需要对每一个「客户」实例进行权限判断,当「客户」数量很大时,这种做法执行效率会降低。第三种做法相当于直接从外部获取「客户」的全部信息,相对前两种效果更好一些,即实现了代码分离,又降低了内存和计算量的消耗但是「客户」对象被暴露了多次,第一次在权限判定时,第二次在业务逻辑时,这样同样降低了效率。同时,这三种方式都需要我们从头开始编码,所以这些方式都不是最佳方式。

Spring Security 为我们提供了一种便捷的域对象安全管理策略,本节主要讨论域对象的权限策略。

2. 域对象安全概述

Spring Security 的域对象安全实现是通过 「ACLs(access control list)服务」方式实现。使用 Spring Security ACLs 服务,需要导入 spring-security-acl-xxx.jar 依赖包。

Spring Security 域对象安全功能以 ACL 的概念为核心,系统中每一个域对象实例都拥有各自的 ACL 配置表,该 ACL 记录着该域访问者的黑白名单列表。Spring Security 的 ACL 有三个主要操作:

  • 查询和修改所有域对象的 ACL 配置
  • 在方法调用前,确保其主体参数可以被进行权限判定;
  • 在方法调用后,确保其主体返回可以被进行权限判断。

这种方法的优势在于 ACL 的存储和检索的高效性。系统中域对象的每个实例都可能被多次访问,ACL 提供了高性能的查询能力、可插拔、最小化死锁的数据库修改操作、代码独立及完整的封装。

图片描述

2.1 ACL 的存储

以数据库方式为例,使用数据库作为 ACL 存储时,需要用到四个数据表:

  • ACL_SID

    系统中任何身份或者权限信息,都有一个 SID,即他的安全唯一标识。该表包含列「ID」,文本类型,用于存储 SID 值;和一个标志列「Flag」,用来描述该 SID 是身份或是权限。因此,每一个身份或者权限都只有一条数据,用来获取授权,SID 也被称为「接收者(recipient)」

  • ACL_CLASS

    用于标识域对象类型。包含列 ID 和域对象的 Java 类名。每一个域对象类名只有一条 ACL 记录。

  • ACL_OBJECT_IDENTITY

    保存着系统里的所有域对象实例,包含列「ID」、「ACL_CLASS.ID」、「ACL_SID.ID」。

  • ACL_ENTRY

    保存着独立的许可记录。包含外键「ACL_OBJECT_IDENTITY.ID」,标识列表示是否允许或者拒绝,标识的格式是二进制的位掩码形式。

ACL_ENTRY 中的掩码位标志着是否允许被访问。默认情况下0位代表读、1位代表写、2位代表创建、3位代表删除、4位代表执行。

2.2 ACL 主要对象和接口

  • ACL。每个域对象都有且仅有一个「ACL」对象,该对象保持了 AccessControlEntry 及「ACL」的所有者。「ACL」不直接引用域对象,而是引用 ObjectIdentity,存储在 ACL_OBJECT_IDENTITY 表中。
  • AccessControlEntry。「ACL」中包含多个 AccessControlEntry 对象,在框架中被简写成 ace。每个 ace 关联 PermissionSidACL 的实例。ace 可以标记为许可,也可以标记为不允许,被存储在 ACL_ENTRY 表中。
  • Permission。权限表示一个特定的不可变的位掩码,具有匹配权限和信息输出的功能。基本权限策略(0 位~4 位)包含在 BasePermission 类中。
  • Sid。「ACL」模块需要用到用户的身份信息和权限信息。这些信息通过 Sid (Security identity)定位。常见的身份信息 Sid 类如 PrincipalSidGrantedAuthoritySid。这些信息存储在 ACL_SID 表中。
  • ObjectIdentity。每个域对象在「ACL」模块内部用 ObjectIdentity 表示。默认实现类为 ObjectIdentityImpl
  • AclService。检索适用于给定 ObjectIdentityAcl 实例。其实现类有 JDBCAclService 等,检索操作委托给 LookupStrategy 完成。LookupStrategy 为检索「ACL」信息提供了一种高度优化的策略,使用批处理检索的方式「BasicLookupStrategy」,并支持利用视图、分级查询及其他高性能方案的「non-ANSI SQL」方式实现。
  • MutableAclService。允许「ACL」被修改变动。该接口如果不是必须的。

注意:现有的 AclService 及其数据库相关类,使用的都是 ANSI-SQL

3. 代码演示

Spring Security 官方提供了两个实例,它们演示了ACL模块。第一个是关于联系人的演示,第二个是文档管理系统(DMS)案例。

使用 Spring Security ACL 功能的第一步,是确定 ACL 数据的存储位置。这里需要实例化 DataSource,并将其注入到 JdbcMutableAclServiceBasicLookupStrategy 实例中。前者提供了修改的接口,后者用于提高「ACL」检索效能。

当上述内容完成实例化之后,接下来我们需要确保域模型和 Spring Security ACL 的连通性。多数情况下域对象都包含 public Serializable getId() 方法,用来返回域对象的唯一标识。

关于如何创建「ACL」或者修改现有「ACL」请看以下代码:

// 为 ACE 准备基本数据
ObjectIdentity oi = new ObjectIdentityImpl(Foo.class, new Long(44));
Sid sid = new PrincipalSid("Samantha");
Permission p = BasePermission.ADMINISTRATION;

// 创建 ACL 对象
MutableAcl acl = null;
try {
acl = (MutableAcl) aclService.readAclById(oi);
} catch (NotFoundException nfe) {
acl = aclService.createAcl(oi);
}

// 通过 ACE 授予更多权限
acl.insertAce(acl.getEntries().length, p, sid, true);
aclService.updateAcl(acl);

该实例中,演示了如何检索标识符为 44 的类型为 Foo 的域对象。而后我们创造了「ACE」,是名为「Samantha」的主体可以访问和管理该对象。实例中 insertAce 方法的作用是插入条目,其最后一个 bool 值即为「允许」或「拒绝」,通常情况下,我们使用白名单「ACL」方式。

完成了上述内容后,我们需要在数据库中维护好「ACL」信息,并将「ACL」信息作为授权决策逻辑的一部分来使用。

一旦您使用了上述技术在数据库中存储一些ACL信息,下一步就是实际使用ACL信息作为授权决策逻辑的一部分。这一步实现方式有很多,比如扩展 AccessDecisionInvestor 或者 AfterInvocationProvider,可以分别在方法执行前后触发鉴权。这些方法使用 AclService 检索「ACL」,然后调用 Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode) 决定是允许还是拒绝。同样也可以使用 AclEntryVoterAclEntryAfterInvocationProviderAclEntryAfterInvocationCollectionFilteringProvider 类,所有这些类都提供了基于声明的方式去获取 ACL 信息,所以不需要我们每次修改权限代码。

4. 小结

本节讨论了域对象的安全配置策略,主要内容有:

  • Spring Security 通过 ACL 方式实现高性能域对象的权限控制;
  • Spring Security ACL 鉴权有基于关系型数据库的成熟解决方案;
  • Spring Security ACL 模块降低了执行效率,也降低了开发工作量。

至此,关于权限部分的讨论告一段落,从下节开始,我们讨论 Spring Security 除了「认证」和「鉴权」之外的常用操作。