域对象的安全(Domain Object Security)
24.1. 概述
请注意:在2.0.0之前,Spring Security称为Acegi
Security。老的Acegi Security提供了一个ACL模块,放在org.[acegisecurity/springsecurity].acl
包下。这个老的现在已经被抛弃,并可能会在将来的
Spring Security
发行版中去掉。本章讨论新的
ACL
模块,官方推荐在
Spring Security 2.0.0或更高版本中使用,它被置于org.springframework.security.acls
包下。
复杂的应用常常会有需要定义访问权限——不只是在web请求和方法调用水平的控制。而是安全决议需要由“who (Authentication
), where (MethodInvocation
) and what (SomeDomainObject
)”构成。也就是说,授权决议还要考虑方法调用中的实际域对象。
想象你正在为宠物诊所设计一个应用。你的基于Spring的应用主要有两组用户:宠物诊所的职员和客户。职员可以访问所有数据,但客户只能看到自己的客户记录。为了使这个例子更加有趣一点,你的客户可以让其它客户看他们的客户记录,例如他们"puppy
preschool"的顾问或本地"Pony Club"的总裁。以Spring Security为基础,你有几个方式可以选择:
1.
编写一个自己的业务方法强制安全控制。你可以从Customer域对象中获取一个有访问权限用户的集合。可以用SecurityContextHolder.getContext().getAuthentication()
来访问
Authentication
对象。
2.
编写一个
AccessDecisionVoter
,通过
Authentication
中储存的
GrantedAuthority[]
来控制安全检查。这意味着你的
AuthenticationManager
要把代表用户对域对象访问权限的
GrantedAuthority
[]填充到Authentication
对象中去。
3.
编写一个AccessDecisionVoter
来强制安全控制,直接打开目标
Customer
域对象。这意味着你的
voter
需要访问
DAO
来获取
Customer
对象。它接着会访问
Customer
对象集合来进行安全决议。
这些方法都是合情合理的。但是,第一个方法把你的授权检查和业务代码耦合到一起了。带来的主要问题包括提高了单元测试的难度,以及Customer
授权逻辑很难在其它地方重用。从
Authentication
中获取GrantedAuthority[]
是好的,但是面对数量很大的
Customer
s时也是不行的。如果用户访问的Customer
s数量可能达到5,000个(在这个例子里不大可能,但是想象如果是一个很大的Pony Club里的兽医!),构建Authentication
需要耗费的内存和时间可能不是你想要的。最后一种方法,编写代码从外部直接打开
Customer
,可能是这
3
个方法里最好的一个。它成功分离了关注点,也没有滥用内存和
CPU
,但还是效率很低,
AccessDecisionVoter
和最终的业务方法都要自己来调用
DAO
方法来获取
Customer
对象。一次业务方法调用要访问两次
DAO
很明显也不是你想要的。另外,上面提到的每一种方法你都要自己从头来编写自己的访问控制列表(
ACL
)持久化和业务逻辑。
幸运地,还有另外一个选择,下面我们来讨论一下这个选择。
24.2. 关键概念
Spring Security的ACL服务发布为spring-security-acl-xxx.jar
。要使用
Spring Security的域对象安全,你要把这个包加入到你的classpath中。
Spring Security的域对象安全以访问控制列表(ACL)为核心概念。你系统里的每个域对象都有自己的ACL,这个ACL记录了谁能或不能访问该域对象。Spring Security为你的应用带来3个主要的ACL相关能力。
·
提供一条可以高效地为你所有的域对象获取ACL条目(以及修改ACL)的途径;
·
提供一条在方法被调用前保证给定用户有访问你的域对象许可的途径;
·
提供一条在方法被调用后保证给定用户有访问你的域对象许可的途径;
正如第一点所说,Spring Security的ACL模块的主要能力之一是提供一个高性能获取ACL的途径。这个ACL repository的能力是非常重要的,因为你系统中的每一个域对象可能有若干个访问控制条目,每个ACL又可能是从其他ACL继承而来——树型结构(这很常用,Spring Security提供了很容易使用的支持)。Spring Security的ACL模块已经很小心的考虑了设计以满足高性能的ACL检索,它利用了可插接的缓存模块,死锁最小化的数据库更新,独立于ORM框架(我们使用直接JDBC),适当的包装,以及透明的数据库更新。
数据库的设计是以ACL模块的操作为中心的,让我们来看看该模块的实现里缺省使用的4个主要数据表。下面列出在一个典型的Spring Security ACL部署中的数据表,以数据量大小排序,行数最多的在最后面:
·
ACL_SID使我们能够在系统中唯一定义任何principal或authority(“SID”表示“Security IDentity”)。主键是ID,SID的文字表示,以及一个表示该SID是principal名还是GrantedAuthority
的标识。每个
principal
或
GrantedAuthority
都有一个对应的记录。当在接受许可的上下文中使用的时候,
SID
通常被称为“接收者(
recipient)”。
·
ACL_CLASS使我们能够在系统中唯一定义任何域对象类。主键是ID,然后是CLASS(java类名)。每个我们想要为其存储ACL许可的类有一个记录。
·
ACL_OBJECT_IDENTITY存储系统里每个域对象实例的信息。其中的列包括ID;一个到表ACL_CLASS 的外键OBJECT_ID_CLASS;一个使我们知道是为哪个ACL_CLASS类实例提供信息的唯一标识OBJECT_ID_IDENTITY;父对象,一个到ACL_SID的外键OWNER_SID表示该域对象的所有者;ENTRIES_INHERITING表示是否允许ACL条目从ACL父条目继承。
·
最后,ACL_ENTRY存储为每个recipient分配的权限。它的列包括一个到表ACL_OBJECT_IDENTITY的外键,recipient(例如一个到ACL_SID的外键),是否需要审计,以及一个表示许可是授予还是拒绝的“整数位掩码”(许可可能包括多个方面,如view, edit, delete)。每个recipient对每个域对象的许可都有一个记录
上一段中提到,ACL系统使用了“整数位掩码”。不要担心,你不需要知道这些数字指向的具体位移来使用ACL系统,只需要知道我们有32个开关可以打开或关闭就可以了(一个int数值4个byte,共32个bits)。每一个位代表一个permission,缺省地,这些permission是:读(bit 0),写(bit 1),创建(bit 2),删除(bit 3)和管理(bit 4)。如果你要使用其他permission,可以很容易的实现自己的permission实例,ACL框架的其余部分不需要具备你定义的扩展的知识,即可完成操作。
明白我们采用这种“整数位掩码”对你的系统中域对象的数量是完全没有影响这一点是很重要的。我们的permission只能使用32个bit,但是你可以有数以十亿计的域对象(这也意味着会有数以十亿计的记录在ACL_OBJECT_IDENTITY表中,ACL_ENTRY表的记录则更多)。我们特别说明这一点是因为发现有些用户错误的以为我们要为每个潜在的域对象使用一个bit,这是不对的。
现在我们已经提供了一个ACL系统的基本概览,以及在表结构上看起来的样子,现在让我们来看看关键接口。关键接口列表如下:
·
Acl:每一个域对象有且仅有一个ACL的对象,它内部拥有AccessControlEntry
,并知道Acl的所有者。Acl不直接指向域对象,而是指向一个ObjectIdentity
。
Acl
是存储在
ACL_OBJECT_IDENTITY表中的。
·
AccessControlEntry
:一个
Acl
包含多个
AccessControlEntry
,在ACL框架中,我们常常把它简称为ACE。每个ACE都指向一组Permission,Sid和Acl。ACE可以是granting或non-granting的,同时包含有审计的相关设置。ACE保存在ACL_ENTRY表中。
·
Permission
:
Permission
表示一个不可变的位掩码,并为位掩码机制和信息输出提供方便的功能。上面提到的
5
个基本的
Permission
(bits 0 到 4)包含在
BasePermission
类中。
·
Sid
:
ACL
模块需要参照到
principal
和
GrantedAuthority[]
。
Sid
接口提供实际安全对象(如
principal, role, group
等等)和
Acl
中实际内容的间接联系,简称“安全身份”。普通的实现包括有
PrincipalSid
(在
Authentication
中表示一个
principal
)和
GrantedAuthoritySid
。安全身份信息放在
ACL_SID表中。
·
ObjectIdentity
:
ACL
模块中,内部用
ObjectIdentity
来表示每个域对象。缺省的实现是
ObjectIdentityImpl
。
·
AclService
:用于获取
ObjectIdentity
适用的
Acl
。在包含的实现(
JdbcAclService
)中,获取操作委派给
LookupStrategy
。
LookupStrategy
为获取
ACL
信息提供高度优化的策略,使用批获取(
BasicLookupStrategy
),也支持用户实现以提供更好的性能,例如层次化查询和类似的以性能为中心的非
ANSI SQL
能力。
·
MutableAclService
:允许修改
Acl
以便进行持久化,如果不是为了这个可以不必使用这个接口。
请注意我们提供的AclService和相关的数据库类都是使用ANSI SQL的。因此可以工作在所有主流数据库服务器上。在编写本文的时候,已经在Hypersonic SQL, PostgreSQL, Microsoft SQL Server 和 Oracle上测试过。
Spring Security发行包中有两个例子可以用来演示ACL模块。其一是Contacts例子,另外一个是Document Management
System (DMS)。我们建议你仔细看看这些例子。
24.3. 入门
要开始使用Spring Security的ACL能力,你需要把你的ACL保存在某个地方。这要通过Spring实例化一个DataSource
。然后该数据源被注入到
JdbcMutableAclService
和
BasicLookupStrategy
。后者提供了高性能的
ACL
获取能力,前者提供了可改变的能力。请参考发行包中任何一个例子来了解如何进行配置。你还需要往上一节中提到的四个表中填充数据(参考
ACL
例子看相关
SQL
语句)。
创建所需数据表并实例化JdbcMutableAclService
后,你还要确认你的领域模型支持与
Spring Security ACL包互操作。很可能ObjectIdentityImpl
已经提供了足够的支持,它提供了多种可用的方式。大部分的域对象会包含一个
public Serializable
getId()
方法。如果返回类型是
long
,或者和
long
兼容(例如
int
),你就不需要烦
ObjectIdentity
的问题了。
ACL
模块的很多部分都依赖于
long identifier
。如果没有使用
long
(或
int
,
byte
等),你有机会要重新实现几个类。我们不打算在
Spring Security ACL模块中支持非
long identifier
,因为
long
和所有的数据库
sequence
兼容,也是最为常用的
identifier
数据类型,同时有足够的长度支持通常的应用场景。
下面的代码展示如何创建一个Acl,或修改一个已存在的Acl:
// 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);
在上面的例子中,我们获取的ACL是和一个标识(identifer)为44的"Foo"域对象联系在一起的。我们增加一个ACE以便一个叫"Samantha"的principal能"administer"该对象。除了insertAce方法,这段代码不需要再多加说明。它的第一个参数是新的ACE要插入到Acl的什么位置。上面的例子中,我们把它放到最后。最后一个参数是一个表示该ACE授权或拒绝的boolean值。大部分时候应该为授权(true),但是如果为拒绝(false),该permission会被有效的关闭。
Spring Security没有在DAO或repository中为自动创建、更新或删除ACL提供任何特别集成。你要为域对象编写像上面这样的代码。在你的业务层中使用AOP来自动集成ACL信息和你业务层操作是值得考虑的。我们发现这是一种很有效的方式。
一旦你已经使用了上面的技术来在数据库中存储一些ACL信息,下一步就是实际如何使用这些ACL信息作为你的授权决议逻辑的一部分。在这里你有好几个选择。你可以分别在方法调用前和调用后编写自己的AccessDecisionVoter
或
AfterInvocationProvider
。这些类应该使用
AclService
来获取相关
ACL
,然后调用
Acl.isGranted(Permission[]
permission, Sid[] sids, boolean administrativeMode)
来决定是授权还是拒绝。你也可以使用我们的
AclEntryVoter
,
AclEntryAfterInvocationProvider
或
AclEntryAfterInvocationCollectionFilteringProvider
类。所有这些类都提供了基于声明的方式,运行时根据这些设定信息来进行评估,把你从编写代码中解放出来。请参考例子应用学习如何使用这些类。