复杂程序常常需要定义访问权限,不是简单的web请求或方法调用级别。而是,安全决议需要包括谁(认证),哪里(MethodInvocation)和什么(一些领域对象)。
换而言之,验证决议也需要考虑真实的领域对象实例,方法调用的主体。
想像我们为宠物店设计一个程序。 在你的基于Spring程序里有两个主要的用户组:宠物商店的工作人员和宠物商店的顾客。 工作人员可以访问所有数据,而你的顾客只能看到他自己的数据。 让它更有趣一点儿,你的客户可以允许其他用户看他自己的数据,比如他们“学龄前小狗”教练,或他们本地“小马俱乐部”的负责人。 以Spring Security为基础,我们可以使用很多方法:
编写你的业务方法来提升安全。
你可以使用一个集合,包含 Customer领域对象实例,来决定哪个用户可以访问。
通过SecurityContextHolder.getContext().getAuthentication(),
你可以得到Authentication对象。
编写一个 AccessDecisionVoter 提升安全性,通过保存在Authentication对象里的GrantedAuthority[]。
这意味着你的AuthenticationManager需要使用自定义GrantedAuthority[]组装这个Authentication,处理每个主体访问的Customer领域对象实例。
编写一个 AccessDecisionVoter 提升安全性,直接打开目标Customer领域对象。
这意味着你的投票者需要访问一个DAO,允许它重审Customer对象。
它会访问用户提交的Customer对象的集合,然后执行合适的决议。
每个方法都是完全可用的。
然而,你的第一种认证会涉及你的业务代码。
它的主要问题是单元测试困难,也很难在其他地方重用Customer的授权逻辑。
从Authentication获得GrantedAuthority[]也还好,但是不适合大规模数量的Customer。
如果用户可以访问五万个Customer(不是在这个例子里,但是想像一下,如果它是一个大型的小马俱乐部),这么大的内存消耗,和时间消耗,建造Authentication是不可取的。
最后一个方法,直接从外部代码打开Customer,可能是三个中最好的了。
它分离了概念,没有滥用内存或CPU周期,但它还是没什么效率,在AccessDecisionVoter和最终业务方法里,它自己会执行一个DAO响应,来重申 Customer对象。
每个方法调用,都要评估两次,非常不可取。
另外,每个方法列出了你需要,从头写自己访问控制列表(ACL)持久化和业务逻辑。
幸运的是,这里有另一个选择,我们在下面讨论。
Spring Security的ACL服务放在spring-security-acl-xxx.jar中。
你需要把这个JAR添加到你的classpath下,来使用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”意思是“安全标识”)。
它包含的列有ID,一个文本类型的SID,一个标志,用来表示是否使用文本显示引用的主体名或一个GrantedAuthority。
因此,对每个唯一的主体或GrantedAuthority都有单独一行。
在使用获得授权的环境下,一个SID通常叫做"recipient"授予者。
ACL_CLASS 让我们在系统中确定唯一的领域对象类。包含的列有ID和java类名。 因此,对每个我们希望保存ACL权限的类都有单独一行。
ACL_OBJECT_IDENTITY 为系统中每个唯一的领域对象实例保存信息。 列包括ID,指向ACL_CLASS的外键,唯一标识,所以我们知道为哪个ACL_CLASS实例提供信息,parent,一个外键指向ACL_SID表,展示领域对象实例的拥有者,我们是否允许ACL条目从任何父亲ACL继承。 我们对每个领域对象实例有一个单独的行,来保存ACL权限。
最后,ACL_ENTRY保存分配给每个授予者单独的权限。 列包括一个ACL_OBJECT_IDENTITY的外键,recipient(比如一个ACL_SID外键),我们是否通过审核,和一个整数位掩码,表示真实的权限被授权或被拒绝。 我们对于每个授予者都有单独一行,与领域对象工作获得一个权限。
像上一段提到的,ACL系统使用整数位掩码。
不要担心,你不需要知道使用ACL系统位转换的好处,但我们有充足的32位可以转换。
每个位表示一个权限,默认授权是可读(位0),写(位1),创建(位2),删除(位3)和管理(位4)。
如果你希望使用其他权限,很容易实现自己的Permission实例,其他的ACL框架部分不了解你的扩展,依然可以运行。
了解你的系统中领域对象的数量很重要,完全用不害怕我们选择使用整数位掩码的事实。 虽然我们有32位可用来作权限,你可能有几亿领域对象实例(意味着在ACL_OBJECT_IDENTITY表中有几亿行,ACL_ENTRY也很可能是这样)。我们说出这点,因为我们有时发现人们犯错误,决定他们为每个潜在的领域对象提供一位,情况并非如此。
现在我们提供了ACL系统可以做的基本概述,它看起来像一个表结构,现在让我们探讨关键接口。 关键接口是:
Acl: 每个领域对象有一个,并只有一个Acl对象,它的内部保存着AccessControlEntry,记住这是Acl的所有者。
一个Acl不直接引用领域对象,但是作为替代的是使用一个ObjectIdentity。
这个Acl保存在ACL_OBJECT_IDENTITY表里。
AccessControlEntry: 一个 Acl 里有多个AccessControlEntry,在框架里常常略写成ACE。
每个ACE引用特别的Permission,Sid和Acl。
一个ACE可以授权或不授权,包含审核设置。
ACE保存在ACL_ENTRY表里。
Permission: 一个 permission 表示特殊不变的位掩码,为位掩码和输出信息提供方便的功能。
上面的基本权限(位0到4)保存在BasePermission类里。
Sid: 这个 ACL 模块需要引用主体和GrantedAuthority[]。
间接的等级由Sid接口提供,简写成“安全标识”。
通常类包含PrincipalSid(表示主体在Authentication里)和GrantedAuthoritySid。
安全标识信息保存在ACL_SID表里。
ObjectIdentity: 每个领域对象放在ACl模型的内部,使用ObjectIdentity。
默认实现叫做ObjectIdentityImpl。
AclService: 重审Acl对应的ObjectIdentity。
包含的实现(JdbcAclService),重审操作代理LookupStrategy。
这个LookupStrategy为检索ACL信息提供高优化策略,使用批量检索(BasicLookupStrategy)然后支持自定义实现,和杠杆物化视图,继承查询和类似的表现中心,非ANSI的SQL能力。
MutableAclService: 允许修改了的 Acl放到持久化中。
如果你不愿意,可以不使用这个接口。
请注意,我们的AclService和对应的数据库类都使用ANSI SQL。 这应该可以在所有的主流数据库上工作。 在写作的时候,系统成功在Hypersonic SQL, PostgreSQL, Microsoft SQL Server 和 Oracle上测试通过。
Spring Security的两个实例演示了ACL模块。 第一个是Contacts实例,另一个是文档管理系统(DMS)实例。 我们建议大家看一看这些例子。
为了开始使用Spring Security的ACL功能,你会需要在一些地方保存你的ACL信息。
有必要使用Spring的DataSource实例。
DataSource注入到JdbcMutableAclService和BasicLookupStrategy实例中。
后一个提供了高性能ACL检索能力,前一个提供变异能力。
参考例子之一,使用Spring Security,为一个例子配置。
你也需要使用四个ACL指定的表建立数据库,这写在最后一章(参考ACL实例,查看对应的SQL语句)。
一旦你创建了需要的结构,和JdbcMutableAclService的实例,你下一个需求是确认你的领域模型支持Spring Security ACL包的互操作。
希望的ObjectIdentityImpl会证明足够,它提供可以使用的大量方法。
大部分人会使用领域对象,包含public Serializable getId()方法。
如果返回类型是long或与long兼容(比如int),你会发现你不需要为ObjectIdentity进行更多考虑。
ACL模块的许多部分对应long标识符。
如果你没有使用long(或int, byte等等),你需要重新实现很多类。
我们不倾向在Spring Security ACL模块中支持非long标识符,因为所有数据库序列都支持,最常用的数据类型标识,也可以容纳所有常用场景的足够长度。
下面的代码片段,显示如何创建一个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,分配给"Foo"领域对象,使用数字44作标识。 我们添加一个ACE,这样名叫"Samantha"的主体可以“管理”这个对象。 代码片段是自解释的,除了insertAce方法。 insertAce方法的第一个参数是Acl里新条目被插入的决定位置。 在上面的的例子里,我们只把新ACE放到以存在的ACE的尾部。 最后一个参数是一个布尔值,显示是否ACE授权或拒绝。 大多数时间,是授权(true),如果它是拒绝(false),权限就会被冻结。
Spring Security 没有提供任何特定整合,自动创建,更新,或删除ACL,作为你的DAO的一部分或资源操作。 作为替代的,你会需要像上面一样为你的单独领域对象写代码。 值得考虑在你的服务层使用AOP,来自动继承ACL信息,使用你的服务层操作。 我们发现以前这是一个非常有效的方式。
一旦,你使用上面的技术,在数据库里保存一些ACL信息,下一步是使用ACL信息,作为授权决议逻辑的一部分。
这里你有一大堆选择。
你可以写你自己的AccessDecisionVoter或AfterInvocationProvider,期待在方法调用之前或之后触发。
这些类使用AclService来检索对应的ACL,然后调用Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode),决定权限是授予还是拒绝。
可选的,你可能使用我们的AclEntryVoter,AclEntryAfterInvocationProvider或AclEntryAfterInvocationCollectionFilteringProvider类。
所有这些类提供一个基于声明的方法,在运行阶段来执行ACL信息,释放你从需要写任何代码。
请参考例子程序,学习更多如何使用这些类。