acegi基于过滤器的设计

 

过滤器是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过滤器技术的使用场合非常广泛。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值