过滤器是Java EE平台中的标准技术,由于Acegi的认证策略是由过滤器驱动的,因此过滤器(Filter)是Acegi的重要支撑技术。也正因为Acegi采用了过滤器驱动整个认证过程,因此Acegi使能应用的便携性能够得到保证。与此同时,与认证源解耦也是Acegi的重要设计策略之一,这也是保证Acegi使能应用具有便携性特质的重要前提,因为实际企业应用会采用各种存储源存储用户的认证和授权信息。
本章内容将围绕Acegi(Spring Security)的认证策略展开论述,其中将主要围绕基于过滤器的设计、与认证源解耦、AcegiSecurityException异常体系展开。另外,我们还将简要介绍Acegi发布版和基代码的下载和安装。
4.1 基于过滤器的设计
过滤器是类似于AOP切面的对象。在Spring AOP领域中,大量的AOP切面能够同时作用于同一业务对象,并为这一对象提供各种基础服务,比如事务服务、安全性服务、日志服务。此时,大量的AOP切面(@AspectJ)构成了一个拦截器链。当客户调用处于这一拦截器链后端的业务对象时,整个调用过程必然会经过这一拦截器链的处理。
类似地,一旦若干过滤器拦截到Web请求时,被保护的目标Web页面也将处在过滤器链(FilterChain)的后端。比如,第3章阐述的acegifirstdemo Acegi使能应用启用了Acegi提供的4个过滤器,它们分别是HttpSessionContextIntegrationFilter、BasicProcessingFilter、ExceptionTranslationFilter、FilterSecurityInterceptor。这4个过滤器构成了一个过滤器链,一旦Web请求到来时,这一过滤器链将拦截到它,图4-1展示了整个处理过程。
图4-1 acegifirstdemo示例中由4个过滤器构成的过滤器链
熟悉Filter的开发者可能会问,过滤器及其映射应该配置在web.xml中,而acegifirstdemo项目却将它们配置在Spring DI容器中,这是怎么回事呢?默认时,过滤器确实应该配置在web.xml中,但一旦需要配置的过滤器数量很多时,而且这些过滤器可能要同DI容器进行交互时,直接配置在web.xml中并不是最佳做法,尽管Acegi也允许开发者将过滤器直接配置在web.xml中。因此,Acegi(Spring Security)基代码于org.acegisecurity.util包中提供了FilterToBeanProxy和FilterChainProxy辅助类,以简化过滤器的配置。开发者需要在web.xml中配置FilterToBeanProxy辅助类,并为它提供targetBean或targetClass参数值对,下面给出了相应的配置信息。
<filter>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetBean</param-name>
<param-value>filterChainProxy</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
在启用Acegi使能应用期间,FilterToBeanProxy会依据targetBean或targetClass取值于当前DI容器中查找到目标受管POJO。比如,在acegifirstdemo Acegi使能应用中,targetBean参数的取值是filterChainProxy,这是Spring DI容器中一个受管POJO的名字,即对应于FilterChainProxy的受管POJO。一旦Web请求到来时,FilterToBeanProxy会将过滤操作(doFilter())委派给它引用的“filterChainProxy”。自然地,FilterToBeanProxy仅仅起到了一个桥梁作用,它本身并不会完成具体的过滤操作。但是,它的引入使得其他过滤器能够享受到依赖注入等Spring特性。
如果开发者打算启用targetClass参数来定位filterChainProxy,则可以采用如下配置。此时,web.xml中不会内置DI容器中受管POJO的名字,从而降低了web.xml同DI配置文件的耦合。但是,FilterToBeanProxy将启动动态类装载行为定位并装载FilterChainProxy类,不同Java EE容器的这一行为是存在不少差别的,尤其是IBM WebSphere,因此我们还是建议开发者尽量采用targetBean参数来显式引用FilterChainProxy实例。注意,如果同时配置了targetBean和targetClass参数,则Acegi会优先使用targetBean参数。
<filter>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.util.FilterChainProxy</param-value>
</init-param>
</filter>
默认时,在启动Acegi使能应用期间,FilterToBeanProxy辅助类会到ServletContext中查找Spring DI容器。另一方面,为在Web应用中装载DI容器,开发者可以使用Spring 1.x/2.x提供的ContextLoaderServlet或ContextLoaderListener。一旦采用ContextLoaderServlet装载Spring DI容器,则FilterToBeanProxy和ContextLoaderServlet的实例化顺序可能会出现不一致。此时,FilterToBeanProxy可能试图在ContextLoaderServlet前完成初始化操作,然而它需要引用到Spring DI容器,而这一DI容器又是由ContextLoaderServlet装载完成的。因此,我们需要启用FilterToBeanProxy的延迟初始化策略,从而保证它能够正确操控到DI容器。下面给出了示例配置。
<filter>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetBean</param-name>
<param-value>filterChainProxy</param-value>
</init-param>
<init-param>
<param-name>init</param-name>
<param-value>lazy</param-value>
</init-param>
</filter>
此时,为web.xml中的FilterToBeanProxy配置了init参数,其取值为lazy。一旦新的Web请求到来时,FilterToBeanProxy才会去查找Spring DI容器,并从中定位到targetBean指定的目标受管POJO。下面给出了FilterToBeanProxy中init()方法的源码。
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
//获得<init-param>取值
String strategy = filterConfig.getInitParameter("init");
//判断init取值是否为lazy
if ((strategy != null) && strategy.toLowerCase().equals("lazy")) {
return;
}
//真正完成初始化工作,即去Spring DI容器中查找到FilterChainProxy实例
doInit();
}
无论是使用ContextLoaderServlet,还是ContextLoaderListener,只要启用了延迟装载,Acegi使能应用的启动便不会出现异常。当然,我们建议尽可能采用ContextLoaderListener装载DI容器,而且此时的web.xml配置信息也更少一些。
4.1.1 接管过滤器的生命周期
由于各过滤器配置在Spring DI容器中,因此DI容器负责了过滤器的生命周期。比如,BasicProcessingFilter的部分源码如下。由于BasicProcessingFilter配置在DI容器中,因此各种Spring回调接口、生命周期服务能够应用到它上。相反,这一过滤器实现的init()和destroy()方法体却为空,而且在启动Acegi使能应用时,这些方法也不会被调用到。由于DI容器接管了BasicProcessingFilter的生命周期,因此Filter接口定义的各个生命周期自然失去了原有的价值,取而代之的是InitializingBean定义的相关回调方法,比如afterPropertiesSet()。
public class BasicProcessingFilter implements Filter, InitializingBean {
......
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.authenticationManager, "An AuthenticationManager is required");
Assert.notNull(this.authenticationEntryPoint, "An AuthenticationEntryPoint is required");
}
public void destroy() {}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//省略了许多内容
......
chain.doFilter(request, response);
}
public void init(FilterConfig arg0) throws ServletException {}
......
}
事实上,Acegi提供的各种过滤器实现类中的init()和destroy()方法体几乎为空,它们都是javax.servlet.Filter要求子类必须提供的方法集合。如果开发者打算让Java EE容器管理过滤器的生命周期,则可以启用lifecycle参数,下面给出了配置示例。
<filter>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetBean</param-name>
<param-value>filterChainProxy</param-value>
</init-param>
<init-param>
<param-name>lifecycle</param-name>
<param-value>servlet-container-managed</param-value>
</init-param>
</filter>
此时,配置在Spring DI容器的过滤器中的init()和destroy()方法将被执行到,因为lifecycle参数取值为servlet-container-managed。FilterToBeanProxy负责调用各过滤器中定义的init()和destroy()方法。特定场合,开发者可能还是需要使用到这一特性,比如现有的某些Filter实现类的源码已经不复存在了,而且它们并不是基于Spring Framework开发的。再比如,开发者扩展了Acegi提供的各种过滤器,此时他们在这些过滤器的init()和destroy()方法中完成了大量的初始化和销毁工作。
第3章的acegifirstdemo仅仅使用到Acegi提供的若干过滤器实现。实际上,针对不同场合,Acegi提供了几十个过滤器实现,本书的后续内容会逐步介绍到剩下的大部分过滤器实现。
4.1.2 于web.xml中直接配置过滤器
当我们在Acegi使能应用中同时使用FilterToBeanProxy和FilterChainProxy辅助类时,web.xml中的配置信息非常少,而且开发者需要在DI容器中配置FilterChainProxy受管POJO。FilterToBeanProxy会将过滤操作请求委派给FilterChainProxy,而FilterChainProxy内部维护了一个过滤器链。正如我们从acegifirstdemo示例看到的一样,在执行doFilter()操作时,FilterChainProxy的doFilter()方法会逐个调用到acegifirstdemo定义的那4个过滤器中的doFilter()方法。下面给出了FilterChainProxy的doFilter()方法源码。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
//判断Web请求是否受到过滤器链的保护
ConfigAttributeDefinition cad = this.filterInvocationDefinitionSource.getAttributes(fi);
//如果未受到保护,则直接绕过当前的过滤器链(VirtualFilterChain)
if (cad == null) {
if (logger.isDebugEnabled()) {
logger.debug(fi.getRequestUrl() + " has no matching filters");
}
chain.doFilter(request, response);
return;
}
//获得当前DI容器中已配置的过滤器集合
Filter[] filters = obtainAllDefinedFilters(cad);
if (filters.length == 0) {
if (logger.isDebugEnabled()) {
logger.debug(fi.getRequestUrl() + " has an empty filter list");
}
chain.doFilter(request, response);
return;
}
//构建过滤器链,并逐个调用到DI容器中已配置的过滤器集合
VirtualFilterChain virtualFilterChain = new VirtualFilterChain(fi, filters);
virtualFilterChain.doFilter(fi.getRequest(), fi.getResponse());
}
如果开发者对过滤器(Filter)、过滤器链(FilterChain)的执行机理很熟悉,则应该能够看到,Acegi提供的FilterChainProxy辅助类手工创建了一个VirtaulFilterChain实例(即过滤器链),这是实现了javax.servlet.FilterChain的一个内部类。VirtaulFilterChain的源代码摘录如下,它位于FilterChainProxy之中。
private class VirtualFilterChain implements FilterChain {
private FilterInvocation fi;
private Filter[] additionalFilters;
private int currentPosition = 0;
public VirtualFilterChain(FilterInvocation filterInvocation, Filter[] additionalFilters) {
this.fi = filterInvocation;
this.additionalFilters = additionalFilters;
}
private VirtualFilterChain() {}
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (currentPosition == additionalFilters.length) {
......
fi.getChain().doFilter(request, response);
} else {
currentPosition++;
......
additionalFilters[currentPosition - 1].doFilter(request, response, this);
}
}
}
如果开发者不打算采用FilterChainProxy辅助类,即不在DI容器中配置FilterChainProxy辅助类实例,比如将filterChainProxy定义从acegi-appContext.xml中注释掉,具体如下。
<!--
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,basicProcessingFilter,
exceptionTranslationFilter,filterInvocationInterceptor
</value>
</property>
</bean>
-->
此时,开发者必须手工在web.xml描述符中逐一配置httpSessionContextIntegration- Filter、basicProcessingFilter、exceptionTranslationFilter、filterInvocationInterceptor等过滤器对应的FilterToBeanProxy实例,配置示例如下。
<filter>
<filter-name>httpSessionContextIntegrationFilter</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetBean</param-name>
<param-value>httpSessionContextIntegrationFilter</param-value>
</init-param>
</filter>
<filter>
<filter-name>basicProcessingFilter</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetBean</param-name>
<param-value>basicProcessingFilter</param-value>
</init-param>
</filter>
<filter>
<filter-name>exceptionTranslationFilter</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetBean</param-name>
<param-value>exceptionTranslationFilter</param-value>
</init-param>
</filter>
<filter>
<filter-name>filterInvocationInterceptor</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetBean</param-name>
<param-value>filterInvocationInterceptor</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>httpSessionContextIntegrationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>basicProcessingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>exceptionTranslationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>filterInvocationInterceptor</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
由于VirtaulFilterChain实例不再负责这4个过滤器构成的过滤器链的创建工作了,因此过滤器链的创建工作移交给了Java EE容器。不幸的是,web.xml的内容迅速膨胀起来,相信开发者都不愿意看到这一场景。实际上,早期的Acegi基代码并不存在FilterChainProxy辅助类,因此早期的Acegi使能应用必须采用这一配置方式,即在web.xml中配置大量的Acegi元数据。看来,我们还是应该在acegi-appContext.xml中启用FilterChainProxy辅助类,如果开发者使用新近的Acegi发布版,则能够享受到FilterChainProxy辅助类带来的便利。
无论是启用FilterChainProxy类,还是直接在web.xml中配置若干FilterToBeanProxy,开发者一定要注意那些过滤器间的声明顺序,这一点非常重要。由于各过滤器的工作职责不同,而且它们之间存在依赖关系,因此要谨慎声明各Acegi过滤器的顺序。本书会在后续章节重点阐述这一问题。
为了达到用户认证和授权目的,开发者需要在Acegi使能应用中配置大量的过滤器,而这些过滤器都是Acegi已提供好的。开发者可以将Acegi提供的过滤器看成AOP技术中的具体AOP切面(@AspectJ)。AOP开发者都知道,各个切面的工作职责各不相同,有些切面负责事务处理,一些切面负责安全性控制,而另一些切面负责日志记录等。类似地,Acegi提供的各个Filter实现承担了不同的任务,这些Acegi过滤器有机地构成了过滤器链,它们共同应对客户的认证和授权请求。
Acegi巧妙地借助于标准过滤器技术实现了安全性控制,这是Acegi中最为重要、明智的设计决定之一。当然,Servlet过滤器技术的使用场合非常广泛。