《Spring Security3》第七章第二部分翻译(高级ACL)(上)
高级ACL话题
一些高级的话题在我们配置ACL环境时略过了,包括处理ACE许可授权,在运行时根据GrantedAuthority确定某种类型的ACL变化是否允许。既然现在我们已经有了一个运行环境,那我们要开始了解这些更高级的话题。
Permission如何工作
许可授权(permission)只不过是简单的逻辑标识符用一个整数的二进制位来表示。一个访问控制条目对于SID的授权是基于逻辑与操作所有应用于这个条目的许可授权得到的位掩码。
默认的Permission实现,即o.s.s.acls.domain.BasePermission定义了一系列的整数值来代表常用的ACL授权。这些整数值对应于单个位设置为整数,所以BasePermission. WRITE的值,对于的整数为1,按位的值就是21或2。它们如下图所示:
你可以看到示例的许可授权掩码有二进制整数值为3,这是因为应用Read和Write许可后就会具有这个许可授权值了。在这个图中,所有标准的单个整数授权值都是在BasePermission中作为静态常量定义的。你可能会回忆起来我们在构建ACL配置练习中在o.s.s.acls.AclEntryVoter里,使用过它们中的一个常量BasePermission.READ。
【BasePermission所包含的逻辑常量只是在访问控制条目中经常用到的,并且在Spring Security中没有特殊的语义。对于非常复杂的ACL实现,创建自己的许可授权是很常见的,以增强独立于域或业务的最佳实践。】
一个经常困扰用户的问题是位掩码实际上是如何应用的,因为很多数据库要么不支持按位逻辑操作要么不支持可伸缩的方式。Spring ACL通过把按位进行许可授权计算放到应用中来解决这个问题,而不是放在数据库中。
了解这个处理过程很重要,在这里我们能够看到AclEntryVoter怎样处理声明在方法上的许可授权(在我们的例子中,通过@Secured注解)到真正的ACL授权。下图阐述了针对安全实体的请求,Spring ACL评估声明的许可权限与相关ACE的过程:
我们可以看到AclEntryVoter依赖实现o.s.s.acls.model.ObjectIdentityRetrievalStrategy和o.s.s.acls.model.SidRetrievalStrategy接口的实现类,以获取适当的ObjectIdentity和Sids进行认证检查。关于这些策略有一个很重要的事情就是基于授权认证的上下文,默认的实现类如何决定要返回的ObjectIdentity和Sids。
ObjectIdentity有两个属性分别为type和identifier,它们是根据运行时要检查的对象得到的,并用来声明ACE条目。默认的ObjectIdentityRetrievalStrategy使用全类名来填充type属性。identifier属性通过调用实际对象实例的Serializable getId()方法得到的结果进行填充(译者注:因此进行ACL授权的对象需要有getId这个方法)。
【为了支持ACL检查你的对象并不需要实现接口,但是需要实现特定签名的一个方法这一点恐怕会让实现Spring Security ACL的开发人员感到惊讶。请事先规划,并确保你的域对象包含此方法!你也可以实现自己的ObjectRetrievalStrategy(或内置实现的子类)来调用你选择的一个方法。但遗憾的是,这个方法的名字和类型是不可配置的。】
不幸的是,AclImpl的实际实现直接将配置的Permission与AclEntryVoter进行对比,但是Permission存储在ACE上在数据库中,不能使用按位逻辑操作。Spring Security社区关于这是否有意为之还有争论,但是不管怎样,当你声明一个拥有组合许可授权的用户时需要特别小心,要么AclEntryVoter必须配置上所有组合许可授权要么ACE需要忽略permission域本来可以配置多个值,而是要在每个ACE上只配置一个许可授权。
如果你想在我们简单的场景下校验它,将我们授给ROLE_ADMIN SID的权限由Read许可修改为Read和Write组合二进制掩码,也就是将其转换成3。这需要修改test-acl-data.sql:
insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) select oi.id, 1, si.id, 3, true, true, true from acl_object_identity oi, acl_sid si where si.sid = 'ROLE_ADMIN';
如果现在你作为管理员访问ACL保护的分类,你会被拒绝,即使我们已经在单个ACE上声明了你可以进行Read和Write。(译者注:不得不说,在一点上Spring Security ACL有点弱。)
自定义ACL permission声明
正如在讨论许可授权声明时所讲的那样,许可授权(permission)只不过是带有逻辑名的整数值。既然如此,可以扩展BasePermission来声明自己的许可权限。这里我们会涉及一个很简单的场景,创建一个新的ACL许可权限名为ADMIN_READ。这个权限只会授予给管理员用户,并分配给只有管理员才能读的资源。尽管这对于JBCP Pets站点是一个比较牵强的例子,但是这种类型的自定义权限在处理个人信息时很常用(如社会保险号等——可以回忆下我们在第一章:一个不安全应用的剖析中提到的PII)。
让我们开始要支持这个所要进行的变化。第一步就是用我们的com.packtpub.springsecurity.security.CustomPermission来扩展BasePermission:
package com.packtpub.springsecurity.security; // imports omitted public class CustomPermission extends BasePermission { protected CustomPermission(int mask, char code) { super(mask, code); } protected CustomPermission(int mask) { super(mask); } public static final Permission ADMIN_READ = new CustomPermission(1 << 5, 'M'); // 32 }
接下来,我们要扩展o.s.s.acls.domain.PermissionFactory的默认实现o.s.s.acls.domain.DefaultPermissionFactory,来注册我们的自定义许可权限逻辑值。PermissionFactory的角色是将许可的二进制掩码转换成逻辑许可值(它可以在应用的其它部分被常量值或通过名字引用,如ADMIN_READ)。PermissionFactory需要所有的自定义许可权限在这里进行注册以便于查找。
我们实现com.packtpub.springsecurity.security.CustomPermissionFactory class类,如下:
package com.packtpub.springsecurity.security; // imports omitted public class CustomPermissionFactory extends DefaultPermissionFactory { public CustomPermissionFactory() { super(); registerPublicPermissions(CustomPermission.class); } public CustomPermissionFactory(Class<? extends Permission> permissionClass) { super(permissionClass); } public CustomPermissionFactory( Map<String, ? extends Permission> namedPermissions) { super(namedPermissions); } }
我们可以看到增强了默认的构造方法以调用注册CustomPermission,使其作为一个可用的许可授权。
【在本章的练习中我们不会强调基类的所有可用代码,但是建议你查看父类的其它功能并了解其在ACL系统中其它方面是如何使用的。例如,我们能够看到buildFromName方法在使用ACL自动以JSP tag中用到,这部分我们稍后展示。】
我们需要配置CustomPermissionFactory并将其织入到BasicLookupStrategy。在dogstore-base.xml文件中要做以下的修改:
<bean class="org.springframework.security.acls.jdbc.BasicLookupStrategy" id="lookupStrategy"> <constructor-arg ref="dataSource"/> <constructor-arg ref="aclCache"/> <constructor-arg ref="aclAuthzStrategy"/> <constructor-arg ref="aclAuditLogger"/> <property name="permissionFactory" ref="customPermissionFactory"/> </bean> <bean class="com.packtpub.springsecurity.security.CustomPermissionFactory" id="customPermissionFactory"/>
现在,我们的自定义ACL权限在ACL框架中可用了。接下来,我们要添加一个新的管理员,他被明确分配这个权限到第二个分类“Dog Food”上。我们要添加以下内容到test-acl-data.sql上,以完成这个新授权的需要:
-- User SID insert into acl_sid (principal, sid) values (true, 'admin2'); -- Category #2 insert into acl_object_identity (object_id_class,object_id_ identity,parent_object,owner_sid,entries_inheriting) select cl.id, 2, null, sid.id, false from acl_class cl, acl_sid sid where cl.class='com.packtpub.springsecurity.data.Category' and sid.sid='admin2'; -- Give user 'admin2' access to category 2 -- "32" == 1 << 5 insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) select oi.id, 2, si.id, 32, true, true, true from acl_object_identity oi, acl_sid si where si.sid = 'admin2' and oi.object_id_identity = 2; commit;
你可以看到新的整数二进制掩码值32已经被ACE数据引用了——这会对应我们在代码中定义的新ADMIN_READ权限。在ACL_OBJECT_IDENTITY表中,“Dog Food”分类被它的主键(在object_id_identity列中)值2所引用。
我们还需要在dogstore-base.xml文件中声明一个新的AclEntryVoter。
<bean class="org.springframework.security.acls.AclEntryVoter" id="adminResourceReadVoter"> <constructor-arg ref="aclService"/> <constructor-arg value="VOTE_ADMIN_READ"/> <constructor-arg> <array> <util:constant static-field="com.packtpub.springsecurity.security.CustomPermission.ADMIN_READ"/> </array> </constructor-arg> <property name="processDomainObjectClass" value="com.packtpub.springsecurity.data.Category"/> </bean>
除此以外,我们需要将这个投票器添加到访问决策管理器上,这个管理器负责在ACL保护方法的场景中,进行授权决策:
<bean class="org.springframework.security.access.vote.AffirmativeBased" id="aclDecisionManager"> <property name="decisionVoters"> <list> <ref bean="categoryReadVoter"/> <ref bean="adminResourceReadVoter"/> </list> </property> </bean>
最后,我们需要在方法声明本身上添加需要的角色,在IProductService接口声明上:
public interface IProductService { // other methods omitted @Secured({"VOTE_CATEGORY_READ","VOTE_ADMIN_READ"}) public Collection<Item> getItemsByCategory(Category cat); }
在所有配置完成后,我们可以启动站点并测试ACL权限。基于已配置的示例数据,以下不同用户点击分类时,应该发生的结果:
用户名 |
Pet apparel (分类1) |
Dog Food (分类 2) |
其它分类 |
admin |
允许(通过ROLE_ADMIN SID ACE拥有READ权限) |
拒绝 |
允许 |
admin2 |
允许(通过ROLE_ADMIN SID ACE拥有READ权限) |
允许(通过安全实体SID ACE拥有ADMIN_READ权限) |
允许 |
guest |
拒绝 |
拒绝 |
允许 |
可以看到即使使用我们简单的例子,也能扩展Spring ACL的功能。当然我们使用了很有限的方式来说明这个定义良好的访问控制系统的强大功能,这个系统是建立在安全实体、GrantedAuthority、单个域对象以及业务方法之上。
在JSP中使用Spring Security JSP tag库启动ACL
在第三章:增强用户体验和第五章中看到,Spring Security的JSP tag库提供了暴露认证相关的数据给用户的功能以及基于各种规则限制能看到的内容。
相同的tag库也能够内置地与使用ACL的系统交互。从我们上面简单的例子中,我们已经围绕首页上前两个分类配置了一个简单的ACL授权场景。
让我们更进一步,使用<accesscontrollist>tag来隐藏用户实际不能访问的分类。
请参考我们前面的表格来了解到此为止,我们已经配置的访问规则。
我们将显示每个分类用<accesscontrollist>tag包围起来,声明对这个显示对象要进行的权限检查:
<c:forEach var="category" items="${categories}"> <security:accesscontrollist hasPermission="READ,ADMIN_READ" domainObject="${category}"> <li><a href="category.do?id=${category.name}">${category.name} </a></li> </security:accesscontrollist> </c:forEach>
请想一下,我们期望在这里发生什么——我们想要用户只能看到他有READ或ADMIN_READ(我们自定义的许可权限)权限的条目。所以我们声明了一个逗号分隔的权限列表以及要进行检查的域对象(通过JSP EL的表达式${category}来指定)。
在背后,这个tag的实现使用我们前面讨论的相同SidRetrievalStrategy和ObjectIdentityRetrievalStrategy,所以它与方法安全中使用的ACL具有相同的访问检查计算流程。
推荐阅读
-
《Spring Security3》第六章第二部分翻译(自定义AuthenticationProvider)
-
《Spring Security3》第四章第三部分翻译上(配置安全的密码)
-
《Spring Security3》第六章第二部分翻译(自定义AuthenticationProvider)
-
《Spring Security3》第二章第一部分翻译
-
《Spring Security3》第四章第一部分翻译上(数据库管理信息)
-
《Spring Security3》第四章第二部分翻译(JdbcDaoImpl的高级配置)
-
《Spring Security3》第三章第二部分翻译(退出功能的实现)
-
《Spring Security3》第三章第三部分翻译上(Remember me功能实现)
-
《Spring Security3》第五章第二部分翻译下(实现授权精确控制的方法——页面级权限)
-
《Spring Security3》第二章第二部分翻译