域对象安全
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
关联Permission
、Sid
、ACL
的实例。ace
可以标记为许可,也可以标记为不允许,被存储在ACL_ENTRY
表中。 - Permission。权限表示一个特定的不可变的位掩码,具有匹配权限和信息输出的功能。基本权限策略(0 位~4 位)包含在
BasePermission
类中。 - Sid。「ACL」模块需要用到用户的身份信息和权限信息。这些信息通过 Sid (Security identity)定位。常见的身份信息 Sid 类如
PrincipalSid
和GrantedAuthoritySid
。这些信息存储在ACL_SID
表中。 - ObjectIdentity。每个域对象在「ACL」模块内部用
ObjectIdentity
表示。默认实现类为ObjectIdentityImpl
。 - AclService。检索适用于给定
ObjectIdentity
的Acl
实例。其实现类有JDBCAclService
等,检索操作委托给LookupStrategy
完成。LookupStrategy
为检索「ACL」信息提供了一种高度优化的策略,使用批处理检索的方式「BasicLookupStrategy」,并支持利用视图、分级查询及其他高性能方案的「non-ANSI SQL」方式实现。 - MutableAclService。允许「ACL」被修改变动。该接口如果不是必须的。
注意:现有的 AclService
及其数据库相关类,使用的都是 ANSI-SQL
。
3. 代码演示
Spring Security 官方提供了两个实例,它们演示了ACL模块。第一个是关于联系人的演示,第二个是文档管理系统(DMS)案例。
使用 Spring Security ACL 功能的第一步,是确定 ACL 数据的存储位置。这里需要实例化 DataSource
,并将其注入到 JdbcMutableAclService
和 BasicLookupStrategy
实例中。前者提供了修改的接口,后者用于提高「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)
决定是允许还是拒绝。同样也可以使用 AclEntryVoter
,AclEntryAfterInvocationProvider
,AclEntryAfterInvocationCollectionFilteringProvider
类,所有这些类都提供了基于声明的方式去获取 ACL 信息,所以不需要我们每次修改权限代码。
4. 小结
本节讨论了域对象的安全配置策略,主要内容有:
- Spring Security 通过 ACL 方式实现高性能域对象的权限控制;
- Spring Security ACL 鉴权有基于关系型数据库的成熟解决方案;
- Spring Security ACL 模块降低了执行效率,也降低了开发工作量。
至此,关于权限部分的讨论告一段落,从下节开始,我们讨论 Spring Security 除了「认证」和「鉴权」之外的常用操作。