方法安全的高级知识
方法安全的表现力不仅局限于简单的角色检查。实际上,一些方法安全的注解能够完全使用 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 返回类型的方法中使用。
l filterObject 对于 Collection 中的每一个元素都会重新绑定到 SpEL 上下文中。这意味着,如果你的方法返回了包含 100 个元素的 Collection , SpEL 表达式将会对每一个进行求值。
l SpEL 表达式必须返回一个 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 的访问控制列表(域对象模型)模块。保证是令人兴奋的话题。