支持ACL的Spring表达式语言
SpEL对ACL系统的支持仅限于方法安全,通过使用hasPermission SpEL方法。典型情况下,这种类型的访问检查会与引用一个或多个传入参数(进行@PreAuthorize检查)或集合过滤(进行@PostAuthorize检查)联合使用。
遗憾的是,启用ACL方法安全配置需要我们配置所有的方法安全以明确的Spring Bean的方式。这样,我们就需要我们移除<global-method-security>元素并替换为我们在第六章中介绍过的明确方式配置方法安全。鉴于我们这里不想重复配置(尽管它包含在本章代码中),我们所要做的只是做一下如下的细微修改,ACL hasPermission检查所需要的类就可用了:
<bean class="org.springframework.security.access.expression.method. DefaultMethodSecurityExpressionHandler" id="methodExprHandler"> <property name="permissionEvaluator" ref="aclPermissionEvaluator"/> </bean> <bean class="org.springframework.security.acls.AclPermissionEvaluator" id="aclPermissionEvaluator"> <constructor-arg ref="aclService"/> <property name="permissionFactory" ref="customPermissionFactory"/> </bean>
我们更新的methodExprHandler的bean定义,配置了o.s.s.access.PermissionEvaluator的实现(默认的PermissionEvaluator实现会拒绝所有的许可认证检查)。o.s.s.acls.AclPermissionEvaluator使用了AclService及相关的类去实际检查SpEL表达式声明的许可权限。
随着配置完成,我们可以使用一个替代的方法来过滤显示在首页上的分类列表(确保你已经移除了上一个练习所添加的tag库)。
【注意,hasPermission方法不再支持逗号分隔的许可授权(正如我们在<accesscontrollist> JSP tag中见到的那样),所以我们需要使用SpEL的布尔逻辑。】
只需简单的添加以下声明到IProductService接口上:
@PostFilter("hasPermission(filterObject, 'READ') or hasPermission(filterObject, 'ADMIN_READ')")
Collection<Category> getCategories();
现在,重启应用并比较以匿名用户、admin、admin2进入时首页的分类列表。注意这个显示列表是如何通过ACL许可授权完成的?我们可以看到SpEL的hasPermission方法很好的与使用ACL的应用通过方法安全注解结合起来。
易变的ACL(Mutable ACLs)和授权
尽管JBCP Pets站点没有实现完整的用户用户管理功能,但是你的应用可能会有这些通用的功能如:新用户注册以及管理用户维护。到此时,缺少这些功能——我们是通过在应用初始化的时候用SQL插入的方法代替的——并没有阻止我们演示Spring Security和Spring ACL的很多功能。
但是,在运行时适当的处理声明的ACL、添加或删除系统用户,对于基于ACL的授权环境来说保证一致性和安全很重要。Spring ACL通过易变的ACL(o.s.s.acls.model.MutableAcl)来解决这个问题。
扩展自Acl接口的MutableAcl允许运行时操作ACL域以实现修改某个特定ACL的内存表现。这个功能包括新增、更新以及删除ACE,修改ACE的拥有者和其它有用的功能。
那么,我们可能会期望Spring ACL模块能够有内置的方式将运行时的ACL变化持久化到JDBC数据存储中,它确实如此。o.s.s.acls.jdbc.JdbcMutableAclService能用来创建、更新和删除数据库中的MutableAcl实例,并且管理其它支持ACL的表(能够处理SID,ObjectIdentity以及域对象类名)。
对我们来说,它只需要一个简单的配置来使用JdbcMutableAclService替换JdbcAclService——易变service需要一个对ACL缓存的引用,这样它就能够在更新数据库条目的时候将缓存删除。这个bean的配置很简单:
<bean class="org.springframework.security.acls.jdbc.JdbcMutableAclService" id="mutableAclService"> <constructor-arg ref="dataSource"/> <constructor-arg ref="lookupStrategy"/> <constructor-arg ref="aclCache"/> </bean>
请回忆在本章前面,AclAuthorizationStrategyImpl允许我们指定对易变ACL进行操作所需要的角色。它们以bean配置的方式提供给构造方法。构造参数以及它们的意义,如下:
参数序号 | 做什么的 |
1 | 表明安全实体要修改ACL保护对象拥有者所要拥有的角色 |
2 | 表明安全实体要修改ACL保护对象审计所要拥有的角色 |
3 | 表明安全实体要对ACL保护对象进行其它修改(新建、更新和删除)所要拥有的角色 |
译者注:以上的三个参数的作用就是要保证对保护的对象进行操作时,登录人必须有足够的权限。
JdbcMutableAclService包含一系列的方法在运行时来操作ACL和ACE数据。尽管这些方法本身很容易理解(createAcl,updateAcl,deleteAcl),但是正确的配置和使用JdbcMutableAclService经常对Spring Security的高级用户都很困难。
让我们实现一个ACL启动类它会替换我们的启动脚本,并用代码的方式插入当前的ACL和ACE条目(以及支持的ObjectIdentity和Sid)。
配置Spring事务管理器
JdbcMutableAclService使用Spring的JdbcTemplate来与JDBC DataSource进行交互。所以,它需要一个Spring JDBC PlatformTransactionManager以保证(很有侵入性)所有与数据库的交互正确地包装在事务中。
大多数实际使用Spring的应用可能已经有一个声明的事务管理器,但是我们的JBCP Pets应用并没有。我们在dogstore-base.xml添加一个:
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
声明一个对启动类的应用,一会将会对它进行编码:
<bean class="com.packtpub.springsecurity.security.AclBootstrapBean" init-method="aclBootstrap"/>
很像在第四章:凭证安全存储中的DatabasePasswordSecurerBean,我们配置这个bean使得Spring ApplicationContext初始化时,aclBootstrap方法会被触发——恰当的时间来启动我们应用中的ACL数据。
与JdbcMutableAclService交互
现在,我们要编写启动bean——创建com.packtpub.springsecurity.security.AclBootstrapBean类。首先我们使用@Autowired注入需要的依赖:
package com.packtpub.springsecurity.security;
// imports omitted
public class AclBootstrapBean {
@Autowired
MutableAclService mutableAclService;
@Autowired
IProductDao productDao;
@Autowired
PlatformTransactionManager transactionManager;
接下来是方法的实际定义,它将会被触发以启动ACL数据——我们将会每次分析一个片段:
public void aclBootstrap() {
// domain data to set up
Collection<Category> categories = productDao.getCategories();
Iterator<Category> iterator = categories.iterator();
final Category category1 = iterator.next();
final Category category2 = iterator.next();
我们需要引用实际要保护的域对象以便于为它们创建ACL。回忆一下,我们的ACL启动SQL只对前两个分类添加条目,所以我们要把它们从ProductDAO中取出来。
JdbcMutableAclService需要使用ObjectIdentity来创建初始的MutableAcl(它能够被进一步的管理,我们等会能看到)。为了创建ObjectIdentity,我们需要真正的域对象。
// needed because MutableAclService requires a current authenticated principal
GrantedAuthorityImpl roleUser = new GrantedAuthorityImpl("ROLE_USER");
GrantedAuthorityImpl roleAdmin = new GrantedAuthorityImpl("ROLE_ADMIN");
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("admin","admin",Arrays.asList(new GrantedAuthority[]{roleUser, roleAdmin}));
SecurityContextHolder.getContext().setAuthentication(token);
JdbcMutableAclService会验证一个用户已经登录,并将其作为建立的MutableAcl的默认拥有者。遗憾的是,这是一个强制检查,即便随后你明确设置ACL拥有者(也不行)。因为这些代码执行时没有认证过的用户,所以我们让JdbcMutableAclService把admin用户作为当前认证过的用户(译者注:就是上面最后两句代码)。
// sids
final Sid userRole = new GrantedAuthoritySid("ROLE_USER");
final Sid adminRole = new GrantedAuthoritySid("ROLE_ADMIN");
// users
final Sid adminUser = new PrincipalSid("admin");
final Sid admin2User = new PrincipalSid("admin2");
我们需要为安全实体和组创建Sid,它们是ACL和ACE的结构,所以在这里我们明确创建它们。记住的是,如果你在应用范围内管理ACL(例如,通过UI层调用业务服务完成),你可以以不同的方式处理这个问题——这里的代码只是一个例子,我们鼓励你根据情况作出调整。
// all interaction with JdbcMutableAclService must be within a
transaction
TransactionTemplate tt = new TransactionTemplate(transactionManager);
tt.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus arg0)
{
// category 1 ACL
MutableAcl createAclCategory1 = mutableAclService.createAcl(new Obje
ctIdentityImpl(category1));
createAclCategory1.setOwner(adminRole);
createAclCategory1.insertAce(0, BasePermission.READ, adminRole,
true);
mutableAclService.updateAcl(createAclCategory1);
// category 2 ACL
MutableAcl createAclCategory2 = mutableAclService.createAcl(new Obje
ctIdentityImpl(category2));
createAclCategory2.setOwner(admin2User);
createAclCategory2.insertAce(0, CustomPermission.ADMIN_READ,
admin2User, true);
mutableAclService.updateAcl(createAclCategory2);
}});
SecurityContextHolder.clearContext();
}
}
在一个新的事务范围内,与JdbcMutableAclService的交互完成了。我们可以看到初始的createAcl调用返回一个MutableAcl。在这背后,针对ObjectIdentity和Sid进行了数据库插入和查找。MutableAcl本身提供了方法来创建、更新和删除ACL中的ACE(记住,ACE声明的是单个的权限-SID匹配)。最后MutableAcl通过updateAcl方法调用更新到数据库中。
要注意的是JdbcMutableAclService还负责确保MutableAcl操作进行时,AclCache被更新。
建议你体验易变的ACL服务并增强这个站点以支持用户的新建和编辑——JdbcMutableAclService有很好的文档(在代码层面),尝试实现它会是一个很好的练习,这取决于你是否愿意实现完整的运行时驱动的ACL模型。
Ehcache ACL缓存
Ehcache是一个开源的、内存和基于硬盘的缓存库,它被广泛用在很多开源和商用Java产品中。正如我们在本章前面提到的,Spring Security提供了一个默认的ACL缓存实现,它依赖于一个配置的Ehcache实例,它会存储ACL信息并优先于在数据库中读取ACL。
鉴于在本节我们不想过多介绍详细配置Ehcache,我们将会涉及到Spring ACL如何使用cache并介绍一个简单的默认配置。
配置Ehcache ACL缓存
建立Ehcache很简单——我们需要简单地声明两个Spring Core的bean,它们管理Ehcache实例并暴露几个有用的配置属性:
<bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" id="ehCacheManagerBean"/> <bean class="org.springframework.cache.ehcache.EhCacheFactoryBean" id="ehCacheFactoryBean"> <property name="cacheManager" ref="ehCacheManagerBean"/> </bean>
接下来,我们要实例化Ehcache ACL缓存bean:
<bean class="org.springframework.security.acls.domain.EhCacheBasedAclCache" id="ehCacheAclCache"> <constructor-arg ref="ehCacheFactoryBean"/> </bean>
最后,我们将NullAclCache实现替换为基于Ehcache的实现:
<bean class="org.springframework.security.acls.jdbc.BasicLookupStrategy" id="lookupStrategy"> <constructor-arg ref="dataSource"/> <constructor-arg ref="ehCacheAclCache"/> <constructor-arg ref="aclAuthzStrategy"/> <constructor-arg ref="aclAuditLogger"/> </bean>
当这些配置完成(并且Ehcache运行时JAR在你的classpath中),ACL数据将会基于Ehcache缓存管理器的配置进行缓存。
Spring ACL怎样使用Ehcache
前面介绍的配置步骤可能会比较简单,添加Ehcache到你的应用中——尤其是大量使用——要进行细致的分析,比较缓存的成本和数据库查询的成本。理解Ehcache在Spring ACL中是如何使用的对于规划缓存大小和寿命很重要。
作为ACL缓存策略的一部分,Spring ACL将会存储以下所有的对象(它们的大多数在o.s.s.acls.domain)到缓存中,要么作为key要么作为值:
l ObjectIdentity(实现类为ObjectIdentityImpl);
l Sid(实现类为GrantedAuthoritySid或PrincipalSid);
l Acl(实现类为AclImpl),包含AccessControlEntry (实现类为AccessControlEntryImpl);
l 你对象实例的Serializable类型主键(一般为Long,除非你对Spring ACL运行时类做了重要的个性化)
只有BasicLookupStrategy和MutableAclService是ACL缓存机制的用户,使用缓存很简单。关于缓存中条目的大小,比较好的办法是在合适的负载测试中监控缓存以评估缓存元素的内存使用和存在寿命。
如果你的应用为了其它的目的(如Hibernate或其它ORM根据)已经使用了Ehcache,你可能愿意将用于ORM目的的缓存实例和用于存储ACL数据的缓存实例区分开来。一般的做法是在构建用于Spring ACL的EhCacheFactoryBean时,提供唯一的cache名,如下:
<bean class="org.springframework.cache.ehcache.EhCacheFactoryBean" id="ehCacheFactoryBean"> <property name="cacheManager" ref="ehCacheManagerBean"/> <property name="cacheName" value="springAclCacheRegion"/> </bean>
奇怪的是,除了Javadoc以外,Spring Core中对于Ehcache的支持没有任何正规的文档。如果使用Spring ACL和Ehcache对你很重要,那你很可能需要自己深入研读代码。