Spring Security
参考文档
Part IV. 授权
2.0.x
作者:Ben Alex, Luke Taylor
译者:王耀军(2008.8.8)
翻译说明
2008年8月8日,今晚北京的奥运开幕式即将到来,就在这个下午我终于完成了这份文档的翻译。
Spring Security是个好东西,最近为了新的项目花了些时间来学习,但是网上找不到一个比较好的资料,对授权部分弄不太懂,对着英文版的Reference Documentation,虽然大体能看懂,但是终究理解的不够细致,因此萌发了仔细研读的想法,并籍此机会同时翻译出来,放到网上也好方便后来者。
本文正好与沈哲翻译的Getting Start部分相呼应,一头一尾,中间还有Part II Overall
Architecture 和 Part III Authentication,不知道会不会也有机会被翻译共享出来。
由于本人也是在学习,因此难免翻译会有不当之处,如果你发现了,请不吝赐教,我会更正了重新发出。如果你看了本文觉得有帮助,我也很乐意分享你的喜悦。邮件可以发到wangyaojun@gmail.com。
Part IV. 授权
具备先进的授权处理能力,是Spring
Security之所以如此受欢迎的原因之一。不论你选择如何进行验证——是否使用了Spring Security提供的机制和Provider,或与容器及其它非Spring Security的认证授权机制集成——你会发现可以在你的应用中以一致且简单的方式使用授权服务。
本部分中,我们来研究第一部分中介绍过的AbstractSecurityInterceptor
的不同实现。然后我们看看如何通过域对象的访问控制列表(
ACL)来调整授权。
一般授权概念
22.1. 权限(Authorities)
正如在前面Authentication
部分简单提到过的,所有Authentication
的实现都必须保存一个GrantedAuthority
对象数组。这代表已经授予给
principal
(相当于用户)的权限。
AuthenticationManager
把
GrantedAuthority
对象插入到
Authentication
对象中,然后由
AccessDecisionManager
在做授权决定的时候使用。
GrantedAuthority
只有一个方法接口:
String getAuthority();
AccessDecisionManager
可以通过该方法获取到一个精确代表GrantedAuthority
的字符串。通过返回一个代表字符串这样的方式,使得
GrantedAuthority
可以被大多数
AccessDecisionManager
“读取”到。如果
GrantedAuthority
不能精确地用字符串代表,这样的
GrantedAuthority
被认为是“集合体”,这种情况下
getAuthority()
必须返回
null
。
“集合体”GrantedAuthority
的一个例子如:一个储存了可以分配给不同用户的操作和授权列表的实现。用字符串来表示这样一个集合体
GrantedAuthority
会很复杂,因此
getAuthority()
方法应返回
null
。这告诉
AccessDecisionManager
应该对
GrantedAuthority
实现提供特别支持才能获得具体内容。
Spring Security带有一个GrantedAuthority
的实现——
GrantedAuthorityImpl
。该实现允许把任何用户指定的字符串转换为
GrantedAuthority
。所有
Security Security
框架的
AuthenticationProvider
都是用GrantedAuthorityImpl
来填充
Authentication
对象。
22.2. 调用前处理(Pre-Invocation
Handling)
AccessDecisionManager
由
AbstractSecurityInterceptor
调用,它负责作出最后的访问控制决定。
AccessDecisionManager
接口包括了如下
3
个方法:
void decide(Authentication authentication, Object object, ConfigAttributeDefinition config) throws AccessDeniedException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
从第一个方法可以看出,作出授权评估结果的信息是通过3个参数传递给AccessDecisionManager
的。特别是,通过传入安全对象
(
注:方法中的第二个参数
)
,使得在实际安全对象访问中的参数能被访问到。例如,我们假设安全对象是一个
MethodInvocation
。可以很容易的获取到
MethodInvocation
对象,并从中获得用户参数,然后据此在
AccessDecisionManager
中实现安全逻辑,以确认用户是否被允许进行此操作。如果访问时不被允许的,应该抛出一个
AccessDeniedException
。
supports(ConfigAttribute)
由
AbstractSecurityInterceptor
在开始的时候调用,以便确认
AccessDecisionManager
是否能处理传递过来的
ConfigAttribute
参数。
supports(Class)
方法由安全拦截器的实现调用以确认配置的
AccessDecisionManager
是否支持传递过来的安全对象类型。
用户可以实现自己的AccessDecisionManager
来控制授权的所有方面,
Spring Security包含了几个基于投票策略的AccessDecisionManager
的实现。
图 22.1, “Voting Decision Manager”给出了相关类的关系图。
图 22.1. Voting
Decision Manager
通过这种方式,一系列AccessDecisionVoter
的实现被带进授权决议中。
AccessDecisionManager
这些投票者们的投票决定是否允许访问,如果不允许,抛出一个
AccessDeniedException
。
AccessDecisionVoter
接口也有
3
个类似的方法:
int vote(Authentication authentication, Object object, ConfigAttributeDefinition config);
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
vote方法的具体实现返回一个int值,且必须是AccessDecisionVoter
的
3
个静态变量
ACCESS_ABSTAIN
, ACCESS_DENIED
和ACCESS_GRANTED
之一。如果不能作出任何决定,应该返回
ACCESS_ABSTAIN
表示弃权。如果能做出决定,则必须是
ACCESS_DENIED
(拒绝)
和ACCESS_GRANTED
(授权)二者之一。
Spring Security
提供了3个基于投票策略的AccessDecisionManager
实现。ConsensusBased
基于授权票数与拒绝票数来决定授权与否(不计入弃权票),如果授权数大于拒绝数,则获得授权,否则决绝访问。可以通过设置属性来控制票数相等或全部弃权时的行为。
AffirmativeBased
的策略是,如果有一个或多个投票者投
ACCESS_GRANTED
,则获得授权。跟
ConsensusBased
相似,它也有一个参数可以控制全部投票者弃权时的行为。
UnanimousBased
仅在全部投
ACCESS_GRANTED
票的时候才能获得授权,忽略弃权票(即,只要有一个投拒绝票就拒绝)。跟其他实现类似,它也有一个参数可控制在全部弃权情况下的行为。
用户可以实现自己定制的AccessDecisionManager
,以不同的方式来进行计票。例如,一个特别的
AccessDecisionVoter
可能接受附加的权重值,或者其中有一个投票者可以一票否决。
Spring
Security
提供了两个
AccessDecisionVoter
的实现。
RoleVoter
,会对任何一个以“
ROLE_
”开头的
ConfigAttribute进行投票。如果其中有一个GrantedAuthority
返回的字符串(通过
getAuthority()
方法)与
ConfigAttributes
中一个或多个以“
ROLE_
”开头的一致,则投授权票。如果没有一致的,则投拒绝票。如果没有一个
ConfigAttribute
是以“
ROLE_
”开头的,则投弃权票。
RoleVoter
在进行一致性对比的时候是大小写敏感的。
BasicAclEntryVoter
是另一个
Spring Security
自带的实现。它与
Spring Security
的
AclManager
(后面会提到)集成。该投票者被设计为在一个应用中可以有多个实例,如:
<bean id="aclContactReadVoter"
class="org.springframework.security.vote.BasicAclEntryVoter">
<property name="processConfigAttribute" value="ACL_CONTACT_READ"/>
<property name="processDomainObjectClass" value="sample.contact.Contact"/>
<property name="aclManager" ref="aclManager"/>
<property name="requirePermission">
<list>
<ref local="org.springframework.security.acl.basic.SimpleAclEntry.ADMINISTRATION"/>
<ref local="org.springframework.security.acl.basic.SimpleAclEntry.READ"/>
</list>
</property>
</bean>
<bean id="aclContactDeleteVoter"
class="org.springframework.security.vote.BasicAclEntryVoter">
<property name="processConfigAttribute" value="ACL_CONTACT_DELETE"/>
<property name="processDomainObjectClass" value="sample.contact.Contact"/>
<property name="aclManager" ref="aclManager"/>
<property name="requirePermission">
<list>
<ref local="org.springframework.security.acl.basic.SimpleAclEntry.ADMINISTRATION"/>
<ref local="org.springframework.security.acl.basic.SimpleAclEntry.DELETE"/>
</list>
</property>
</bean>
在上面的例子中,你要定义与MethodSecurityInterceptor
或AspectJSecurityInterceptor
中一些方法对应的
ACL_CONTACT_READ
或ACL_CONTACT_DELETE
。当那些方法被调用的时候,上面定义的相关
voter
会进行投票。
voter
会从方法调用中查找第一个类型为
sample.contact.Contact
的参数,然后把该
Contact
传递给AclManager
。
AclManager
接着会返回一个应用在当前
Authentication
上的访问控制列表(
ACL
)。假设
ACL
包含有需要的
Permission
s中的一个,voter即投授权票。如果ACL不含有任何一个voter定义中需要的permission,volter投拒绝票。BasicAclEntryVoter
是一个非常重要的类,它使你能够建立真正复杂的应用,可以完全地控制在应用上下文中定义的域对象的安全。如果你有兴趣了解更多
Spring Security的ACL能力,以及如何最好地应用它们,请看本参考指南中的ACL部分和"调用后(After Invocation)"部分,然后研究一下Contacts例子应用。
你也可以自己实现定制的AccessDecisionVoter
。
Spring Security的unit
test中提供了几个例子,包括ContactSecurityVoter
和
DenyVoter
。
ContactSecurityVoter
在
CONTACT_OWNED_BY_CURRENT_USER
这个
ConfigAttribute
找不到的时候投弃权票。如果投票,它会从
MethodInvocation
中取得
Contact
对象的
owner
——即该调用的
subject
。如果
owner
与
Authentication
中的用户一致,投授权票。它也可以只是简单把
Contact owner
和
Authentication
中的
GrantedAuthority
进行对比。所有这些只需要简单的几行代码,这充分显示了授权模型的灵活性。
TODO:老的ACL包已经被废弃,去掉这段对老ACL包的描述,改用对新的ACL实现的描述。(即AclEntryVoter)。
22.3. 调用后处理(After
Invocation Handling)
AccessDecisionManager
是在处理安全对象调用之前被AbstractSecurityInterceptor
调用的,而有些应用需要有一条途径可以更改安全对象调用实际返回的对象。你可以很容易的实现自己的
AOP concern
来达到这个目的,
Spring Security提供了一个方便的hook(钩子),同时包括几个具体实现,并与它的ACL能力集成在一起。
图 22.2, “After Invocation
Implementation” 描述Spring Security的 AfterInvocationManager
及其实现.
图 22.2. After
Invocation Implementation
像Spring Security中很多其它部分一样,AfterInvocationManager
有一个单独的具体实现
AfterInvocationProviderManager
,它带有一个
AfterInvocationProvider
列表,其中每个
AfterInvocationProvider
均被允许更改对象或抛出
AccessDeniedException
。确实是多个
provider
均能改变对象,列表中的一个
provider
处理完后把结果提交给下一个
provider
,下一个
provider
在这个结果的基础上进行处理。现在让我们来看一看
AfterInvocationProvider
的
ACL-aware
实现。
请注意如果你在使用AfterInvocationManager
,你仍需要配置属性以便
MethodSecurityInterceptor
的
AccessDecisionManager
允许一项操作。如果你在使用
Spring Security
自带的
AccessDecisionManager
实现,而没有为安全方法调用配置属性,会导致每个
AccessDecisionVoter
都弃权。进而,如果
AccessDecisionManager
的
"allowIfAllAbstainDecisions
"被设置为false,会抛出一个AccessDeniedException
。你可以通过如下两个方法避免这个潜在问题:
(i)把"allowIfAllAbstainDecisions
"设为true(尽管这通常是不推荐的);(ii)简单的确认至少配置了一个属性,使AccessDecisionVoter
会表决,以授予访问权限。后者(推荐)常常通过配置一个
ROLE_USER
或
ROLE_AUTHENTICATED
来实现。
22.3.1. ACL-Aware的AfterInvocationProviders
请注意:Acegi Security 1.0.3包含了一个新的ACL模块的预览。新的ACL模块是现有的ACL模块的重要重写版。新模块可以在org.springframework.security.acls
包中找到,旧的
ACL
模块在
org.springframework.security.acl
下。我们建议用户考虑测试新的ACL模块,并在应用中使用它。老得ACL模块应该被考虑为被抛弃,可能会在将来的发行版中去掉。下面的信息是跟新的ACL包相关的,因此也是被推荐的。
我们几乎都会为普通服务层写的一个方法看起来是这样:
public Contact getById(Integer id);
通常,只有持有读取Contact许可的用户才应被允许获得它。这种情况AbstractSecurityInterceptor
提供的
AccessDecisionManager
这种方法无法满足。这是因为,在安全对象调用之前,只有
Contact
的标识是可用的。
AclAfterInvocationProvider
给出了一个解决方案,其配置如下:
<bean id="afterAclRead"
class="org.springframework.security.afterinvocation.AclEntryAfterInvocationProvider">
<constructor-arg ref="aclService"/>
<constructor-arg>
<list>
<ref local="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/>
<ref local="org.springframework.security.acls.domain.BasePermission.READ"/>
</list>
</constructor-arg>
</bean>
在上面的例子中,Contact
对象将会被获取并递送给
AclEntryAfterInvocationProvider
。如果
Authentication
没有列出的
permissions
其中之一,
provider
会抛出
AccessDeniedException
。
AclEntryAfterInvocationProvider
从
Acl
Service查询以获得Authentication
对该对象访问的ACL。
AclEntryAfterInvocationCollectionFilteringProvider
与
AclEntryAfterInvocationProvider
相似。它用来从
Collection
或
Array
中去除用户没有访问权限的对象。它不抛出
AccessDeniedException
——只是简单而安静的去除不该被访问的元素。该
provider
是这样配置的:
<bean id="afterAclCollectionRead"
class="org.springframework.security.afterinvocation.AclEntryAfterInvocationCollectionFilteringProvider">
<constructor-arg ref="aclService"/>
<constructor-arg>
<list>
<ref local="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/>
<ref local="org.springframework.security.acls.domain.BasePermission.READ"/>
</list>
</constructor-arg>
</bean>
正如你可以想象的,返回的对象是一个Collection或array,该provider才能进行处理。它会去除任何AclManager
指示
Authentication
没有所需
permission
之一的元素。
Contacts例子展示了这两个AfterInvocationProvider
的应用。
22.3.2. ACL-Aware的AfterInvocationProviders (老ACL模块)
请注意:Acegi Security 1.0.3包含了一个新的ACL模块的预览。新的ACL模块是现有的ACL模块的重要重写版。新模块可以在org.springframework.security.acls
包中找到,旧的
ACL
模块在
org.springframework.security.acl
下。我们建议用户考虑测试新的ACL模块,并在应用中使用它。老得ACL模块应该被考虑为抛弃,可能会在将来的发行版中去掉。
我们几乎都会为普通服务层写的一个方法看起来是这样:
public Contact getById(Integer id);
常常,只有持有读取Contact许可的用户才应被允许获得它。这种情况AbstractSecurityInterceptor
提供的
AccessDecisionManager
这种方法无法满足。这是因为,在安全对象调用之前,只有
Contact
的标识是可用的。
BasicAclAfterInvocationProvider
给出了一个解决方案,其配置如下:
<bean id="afterAclRead"
class="org.springframework.security.afterinvocation.BasicAclEntryAfterInvocationProvider">
<property name="aclManager" ref="aclManager"/>
<property name="requirePermission">
<list>
<ref local="org.springframework.security.acl.basic.SimpleAclEntry.ADMINISTRATION"/>
<ref local="org.springframework.security.acl.basic.SimpleAclEntry.READ"/>
</list>
</property>
</bean>
在上面的例子中,Contact
对象将会被获取并递送给
BasicAclEntryAfterInvocationProvider
。如果
Authentication
没有列出的
permissions
其中之一,
provider
会抛出
AccessDeniedException
。
BasicAclEntryAfterInvocationProvider
从
Acl
Service查询以获得Authentication
对该对象访问的ACL。
BasicAclEntryAfterInvocationCollectionFilteringProvider
与
BasicAclEntryAfterInvocationProvider
相似。它用来从
Collection
或
Array
中去除用户没有访问权限的对象。它不抛出
AccessDeniedException
——只是简单而安静的去除不该被访问的元素。该
provider
是这样配置的:
<bean id="afterAclCollectionRead"
class="org.springframework.security.afterinvocation.BasicAclEntryAfterInvocationCollectionFilteringProvider">
<property name="aclManager" ref="aclManager"/>
<property name="requirePermission">
<list>
<ref local="org.springframework.security.acl.basic.SimpleAclEntry.ADMINISTRATION"/>
<ref local="org.springframework.security.acl.basic.SimpleAclEntry.READ"/>
</list>
</property>
</bean>
正如你可以想象的,返回的对象是一个Collection或array,该provider才能进行处理。它会去除任何AclManager
指示
Authentication
没有所需
permission
之一的元素。
Contacts例子展示了这两个AfterInvocationProvider
的应用。
22.4. 授权标签库(Authorization
Tag Libraries)
AuthorizeTag
用来在principal有特定的GrantedAuthority
时显示body内容。
下面的JSP片段展示了如何使用AuthorizeTag
:
<security:authorize ifAllGranted="ROLE_SUPERVISOR">
<td>
<a href="del.htm?id=<c:out value="${contact.id}"/>">Del</a>
</td>
</security:authorize>
如果principal被授予了ROLE_SUPERVISOR,该标签会把标签体内的内容输出。
security:authorize
标签声明了如下属性:
·
ifAllGranted
:列出的所有roles都必须被授予时显示标签体;
·
ifAnyGranted
:列出的任何roles被授予时显示标签体;
·
ifNotGranted
:列出的任何一个roles都没有授予时显示标签体;
你应该知道,每个属性中均可列出多个roles,用逗号隔开,里面的空格会被忽略
这个标签库逻辑上在多个属性间用“AND”来处理。这意味着如果你同时给出了两个或多个属性,所有属性均必须为true,标签才会输出标签体。不要在ifNotGranted="ROLE_SUPERVISOR"
后又加
ifAllGranted="ROLE_SUPERVISOR"
,否则你会永远见不到你的标签体。
通过要求所有属性必须返回true这种方式,授权标签使你可以创建更加复杂的授权场景。例如,为了防止新supervisor看到内容,你可以在同一个tag中声明ifAllGranted="ROLE_SUPERVISOR"
和
ifNotGranted="ROLE_NEWBIE_SUPERVISOR"
。然而,可能简单地使用
ifAllGranted="ROLE_EXPERIENCED_SUPERVISOR"
比在你的设计中插入“
NOT
”条件更好。
最后:该tag是以既定顺序来进行评估的,首先是ifNotGranted
,然后
ifAllGranted
,最后是
ifAnyGranted
。
AccessControlListTag
用来在当前
principal
有
ACL
,表明可访问域对象时显示内容。
如下JSP片段展示了如何使用AccessControlListTag
:
<security:accesscontrollist domainObject="${contact}" hasPermission="8,16">
<td><a href="<c:url value="del.htm"><c:param name="contactId" value="${contact.id}"/></c:url>">Del</a></td>
</security:accesscontrollist>
如果principal有对"contact"于对象的permission
16或permission 1,该tag显示标签体内容。这些数字实际上是BasePermission
的位掩码中用到的整数。请参考本参考指南的
ACL
部分获得更多关于
Spring Security
的
ACL
能力的信息。
AclTag
是老
ACL
模块的一部分,应该考虑为被抛弃。因为历史原因,它与
AccessControlListTag
的工作其实是一样的。