复杂程序常常需要定义访问权限,不是简单的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信息,释放你从需要写任何代码。
请参考例子程序,学习更多如何使用这些类。