方法安全的高级知识
方法安全的表现力不仅局限于简单的角色检查。实际上,一些方法安全的注解能够完全使用Spring表达式语言(SpEL)的强大功能,正如我们在第二章中讨论URL授权规则所使用的那样。这意味着任意的表达式,包含计算、Boolean逻辑等等都可以使用。
使用bean包装类实现方法安全规则
另外一种定义方法安全的形式与XML声明有关,它可以包含在Spring Bean定义中。尽管阅读起来很容易,但是这种方式的方法安全声明在表现性上不如切点,在功能上不如我们已经见过的注解方式。但是,对于一定类型的工程,使用XML声明的方式足以满足你的需求。
我们可以替换前面的例子,将其改成基于XML声明的方式来保护changePassword方法。前面我们已经使用了bean的自动织入,但是这与XML方法包装方式并不兼容,为适应这项技术我们需要明确声明服务层类。
安全包装是安全XML命名空间的一部分。首先我们需要在dogstore-base.xml文件中,包含进来安全的schema,它用来包含安全相关的Spring Bean定义:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:security="http://www.springframework.org/schema/security" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www. springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/jdbc http://www. springframework.org/schema/jdbc/spring-jdbc-3.0.xsd http://www.springframework.org/schema/context http://www. springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/security http://www. springframework.org/schema/security/spring-security-3.0.xsd ">
接下来(为了完成这个练习),移除IUserService.changePassword上的所有注解。
最后,用Spring XML的语法来声明bean,添加如下的附加的包装,它声明任何想触发changePassword方法的人必须是一个ROLE_USER:
<bean id="userService" class="com.packtpub.springsecurity.service.UserServiceImpl"> <security:intercept-methods> <security:protect access="ROLE_USER" method="changePassword"/> </security:intercept-methods> </bean>
像本章前面的其它例子那样,这个保护功能能够很容易地通过将ROLE_USER 改为ROLE_ADMIN并尝试用guest用户账号修改密码来校验。
在背后,这种方式的方法安全保护功能使用了MethodSecurityInterceptor,它被织入到MapBasedMethodSecurityMetadataSource中。拦截器使用它来决定合适的访问ConfigAttributes。不同于可使用SpEL以拥有更强表达能力的@PreAuthorize注解,<protect>声明只能在access属性中有逗号分隔的一系列角色(类似于JSR-250 @RolesAllowed注解)。
可以使用简单的通配符来注明方法名,如,我们可以用如下的方式保护给定bean里所有的set方法:
<security:intercept-methods> <security:protect access="ROLE_USER" method="set*"/> </security:intercept-methods>
方法名匹配可以包含前面或后面的正则表达式匹配符(*)。这个符号的存在意味着要对方法名进行通配符匹配,为所有匹配该正则表达式的方法添加拦截器。注意,其它常用的正则表达式操作符(如?或[)并不支持。请查阅相关的Java文档以理解基本的正则表达式。更复杂的通配符匹配或正则匹配并不支持。
在新代码中这种方式的安全声明并不常见,因为有更富于表现力的方式,但是了解这种方式的安全包装还是有好处的,你可以把它当做方法安全工具栏中的一个可选项。这种方式对于无法为接口或类添加安全注解时特别有效,如当你想为第三方类库添加安全功能时。
包含方法参数的实现方法安全规则
逻辑上,对一些类型的操作来说在制定规则时引用方法的参数是很合理的。例如,我们可能要对changePassword方法进行重新限制,这样试图触发这个方法的用户必须满足两个约束条件:
l 用户试图修改的必须是自己的密码,或者
l 用户是管理员(这种情况下,用户可以修改任何人的密码,这可能会通过一个管理界面)
修改这个规则限制只能管理员触发方法是很容易的,但是对我们来说怎样确定用户试图修改的是自己的密码并不清楚。
幸运的是,Spring Security方法注解所绑定的SpEL支持更复杂的表达式,包括含有方法参数的表达式。
@PreAuthorize("#username == principal.username and hasRole('ROLE_USER')")
public void changePassword(String username, String password);
译者注:个人感觉注解更应该是:@PreAuthorize("#username == principal.username or hasRole('ROLE_USER')")
在这里,你可以看到我们对第一个练习中使用的SpEL指令进行了增强,校验安全实体的用户名与方法参数的用户名一致(#username——方法的参数名有一个#前缀)。方法参数绑定的强大功能可以使你更有创造力并允许对方法的安全保护有更精确的逻辑规则。
参数绑定是如何实现的?
与我们在第二章中<intercept-url>授权表达式的设计类似,一个表达式处理器——o.s.s.access.expression.method.MethodSecurityExpressionHandler的实现类——负责建立SpEL的上下文,表达式基于这个上下文进行求值。MethodSecurityExpressionHandler使用o.s.s.access.expression.method.MethodSecurityExpressionRoot作为表达式根,它(与WebSecurityExpressionRoot为URL授权表达式所做的那样)为SpEL表达式的求值暴露了一系列的方法和伪属性。
与第二章中我们见到过的内置表达式(如hasRole)基本完全一致,这些表达式也能够在方法安全的上下文中使用,只是添加了一个与访问控制列表相关的方法(将在第七章:访问控制列表中介绍)以及另一个用来基于角色过滤数据的伪属性。
你可能注意到在前面的例子中,相对于web层的表达式来说,我们使用的principal伪属性是一个在方法安全表达式中很重要的表达式操作符。principal伪属性将会返回在当前Authentication对象中的principal,一般来讲会是一个字符串(用户名)或UserDetails实现——这就意味着UserDetails的所有属性和方法都能被使用来完善方法的访问限制。
下图展现了这个方面的功能:
SpEL变量的应用要通过#前缀。需要注意的很重要一点是,为了使得方法参数的名字能够在运行时得到,调试符号表中的信息必须在编译后保留。启用这个功能的常见方法如下:
l如果你使用的javac编译器,在构建class使,要加上-g标示;
l如果在ant中使用<javac>任务,添加debug="true"属性;
l在Maven中,在构建你的POM是设置属性maven.compiler.debug=on。
查阅你的编译器、构建工具或IDE文档寻求帮助,以实现在你的环境中有相同的设置。
使用基于角色的过滤保护方法的数据安全
最后两个依赖Spring Security的注解是@PreFilter和@PostFilter,它们被用来对Collections或Arrays (仅@PostFilter有效)使用基于安全的过滤规则。这种类型的功能呢个被称为安全修正或安全修剪(security trimming or security pruning),并且涉及到在运行时使用安全实体的凭证进行集合对象的移除。正如你可能预想的那样,这种过滤通过在注解声明中使用SpEL表达式来实现。
我们将会讲解一个JBCP Pets的例子,在其中我们将会对系统用户显示一个特别的分类,叫做顾客最爱(Customer Appreciation Specials)。另外,我们将会使用Category对象的customersOnly属性来保证特定分类的产品只能对该存储的顾客可见。
对于使用Spring MVC的web应用来说,相关的代码很简单直接。com.packtpub.springsecurity.web.controller.HomeController类用来显示主页,它拥有显示分类——一个包含Category对象的Collection——到用户主页的代码:
@Controller
public class HomeController extends BaseController {
@Autowired
private IProductService productService;
@ModelAttribute("categories")
public Collection<Category> getCategories() {
return productService.getCategories();
}
@RequestMapping(method=RequestMethod.GET,value="/home.do")
public void home() {
}
}
业务层IProductService接口的实现委托给数据访问层IProductDao。简单起见,IProductDao接口的实现类使用了一些硬编码的Category对象。
通过@PostFilter实现基于角色的数据过滤
如同我们在方法安全授权中所作的那样,放置@PostFilter安全过滤指令在业务层上。在本例中,代码如下:
@PostFilter("(!filterObject.customersOnly) or (filterObject.customersOnly and hasRole('ROLE_USER'))")
Collection<Category> getCategories();
在理解它的工作原理之前,我们首先看一下@PostFilter注解的处理流程:
我们可以看到,再次使用了Spring AOP的标准组成,在一个after的AOP处理器中o.s.s.access.expression.method.ExpressionBasedPostInvocationAdvice被执行,为这个增强(advice)被用来过滤目标方法返回的Collection或Array。像@PreAuthorize注解的处理那样,DefaultMethodSecurityExpressionHandler被再次用在这个表达式构建SpEL上下文和求值上。
应用修改后的效果能够在以guest和登录用户访问JBCP Pets时看到。你可以看到,当作为注册用户登录,顾客最爱(Customer Appreciation Specials)分类将会对注册用户可见。
现在,我们已将学习方法后过滤的处理过程,让我们回到所使用的进行分类过滤的SpEL表达式上来。简单起见,我们引用Collection作为方法的返回值,但是@PostFilter可以在Collection和Array返回类型的方法中使用。
lfilterObject对于Collection中的每一个元素都会重新绑定到SpEL上下文中。这意味着,如果你的方法返回了包含100个元素的Collection,SpEL表达式将会对每一个进行求值。
lSpEL表达式必须返回一个Boolean值。如果表达式的求值为true,这个元素将会保留在Collection中,如果表达式求值为false,这个元素将会被移除。
在大多数情况中,你会发现Collection的事后过滤将会为你节省到处书写的大量模板代码。
注意理解@PostFilter在原理上怎样生效,不像@PreAuthorize,@PostFilter指定了方法行为而不是事先条件。一些追求纯正面向对象的人可能会认为@PostFilter包含在方法注解并不合适,而是这样的过滤应该在一个方法实现中通过代码进行处理。
【Collection过滤的安全性。需要注意的是你的方法实际返回的Collection被修改了。在一些场景下,这并不是合适的行为,所以你需要保证你方法返回的Collection能够被安全地修改。如果返回的Collection是ORM绑定的,这一点尤其重要,因为事后过滤的修改可能会无意间持久化到ORM的数据存储中。】
Spring Security还支持事先过滤Collections方法参数的功能,让我们尝试实现一下。
使用@PreFilter实现事先过滤集合
@PreFilter能被用来基于当前的安全上下文过滤传递到方法中的Collection元素。在功能上,只要拥有对Collection的引用,这个注解的行为与@PostFilter除了以下两点外完全一致:
l @PreFilter只支持Collection参数,不支持Array参数;
l @PreFilter使用了一个额外可选的filterTarget属性,如果方法超过一个参数的话,这个属性被用来指明要过滤哪个参数。
同@PostFilter一样,要记住传递到方法中的原始Collection会被永久修改。这可能不是合适的行为,所以你要保证调用者能够了解对Collection的修剪在方法调用后是安全的。
为了展现这个过滤的使用,我们临时修改在@PostFilter注解中用到的getCategories方法,改成把它的过滤委托给一个新的方法。修改getCategories为如下:
@Override
public Collection<Category> getCategories() {
Collection<Category> unfilteredCategories = productDao.getCategories();
return productDao.filterCategories(unfilteredCategories);
}
我们要添加filterCategories方法到IProductDao接口和实现中。@PreFilter注解要加到接口声明中,如下:
@PreFilter("(!filterObject.customersOnly) or (filterObject.customersOnly and hasRole('ROLE_USER'))")
public Collection<Category> filterCategories(Collection<Category> categories);
一旦你添加了该方法和@PreFilter注解声明到接口中,添加一个空实现(尽管你可以在方法中按照业务需要进行进一步的过滤)。添加以下的方法体到ProductDao中:
@Override
public Collection<Category> filterCategories(Collection<Category> categories) {
return categories;
}
到此为止,你可以证实功能在从IProductService接口中移除@PostFilter注解后依旧正常使用,你会发现行为与前面完全一样。
到底为何使用@PreFilter
此时,你可能挠头问@PreFilter到底有什么用处,因为@PostFilter的功能完全一样并适应更多的逻辑场景。
@PreFilter的确有很多用处,有一些与@PostFilter重叠,但是记住当声明安全限制时,宁可多余——我们宁可过于小心也不能有潜在的安全危险。
以下是@PreFilter可能有用的场景:
大多数的应用都在数据层支持基于一系列的参数执行查询。@PreFilter能够保证安全过滤掉传递到数据库查询中的参数。
在很多场景下,业务层收集来自于不同数据来源的信息。每个数据来源的输入能够进行安全的修剪以保证用户不会无意间看到他本不应该看到的搜索结果或数据。
@PreFilter能够用来进行位置或关系相关的过滤——如可以基于用户点击过的分类或购买过的物品组成明确搜索条件的基础。
希望这能够帮助你了解在哪里使用对Collections的事先或事后过滤,以在你的应用中添加额外的保护层。
关于方法安全的警告
请记住这个关于实现和理解方法安全很重要的警告——为了真正很好地实现这个功能强大的安全类型,理解其背后是怎样运行的很重要。缺乏对AOP的理解,不管是概念还是策略层面,都是造成方法安全失败的首要原因。请确保你不仅完整阅读本章,还有Spring 3 Reference Documentation的第七章:使用Spring进行面向方面编程。
在为一个已有系统实现方法安全之前,最好检查应用对面向对象设计原则的遵守情况。如果你的应用已经合理使用了接口和封装,当你实现方法安全时就会有更少的不可预知错误。
小结
在本章中,我们覆盖了Spring Security处理授权的大部分功能。我们已经通过对JBCP Pets在线商店在应用的各个层添加授权检查,学习了足够的知识,可以保证恶意用户不能操控或访问他们无权访问的数据。
尤其,我们:
l学习了在应用设计过程中规划授权、用户/组匹配;
l介绍两种实现细粒度授权的技术,基于授权或其它安全标准过滤出页面内容,使用了Spring Security的JSP标签库和Spring MVC控制器的数据绑定;
l介绍了在业务层保护业务功能和数据的方法,支持丰富且与代码紧密集成的安全模型指令。
到此为止,我们已经介绍到了你在web安全应用开发中所使用到的很多Spring Security重要功能。
如果你一口气读到此处,这是一个很好的时间休息一下,复习我们所学的东西,并花些时间了解实例代码和Spring Security本身的代码。
在接下来的两章中,我们会涵盖高级的自定义和扩展场景,以及Spring Security的访问控制列表(域对象模型)模块。保证是令人兴奋的话题。