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包含有需要的Permissions中的一个,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从AclService查询以获得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从AclService查询以获得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的工作其实是一样的。