狼爱上狸

我胡汉三又回来了

acegi,IBM的Acegi Security System(4)

2008 年 3 月 20 日

Bilal Siddiqui 将继续在他的 系列文章 中展示如何使用 Acegi 保护 Java™Server Faces (JSF) 应用程序。配置 JSF 和 Acegi,让它们在 servlet 容器中协作,探索 JSF 和 Acegi 组件如何彼此协作。

系列 的前 3 部分讨论了如何使用 Acegi Security System 保护 Java 企业应用程序:

  • 第 1 部分 解释了如何使用 Acegi 的内置过滤器实现一个简单的基于 URL 的安全系统。
  • 第 2 部分 展示了如何编写访问控制策略、将其存储在 LDAP 目录服务器中,以及配置 Acegi 与 LDAP 服务器交互,从而实现访问控制策略。
  • 第 3 部分 展示了如何在企业应用程序中使用 Acegi 保护对 Java 类实例的访问。

第 4 部分将讨论如何使用 Acegi 保护在 servlet 容器中运行的 JavaServer Faces (JSF) 应用程序。本文首先解释 Acegi 针对此目标提供的特性,并澄清一些关于使用 Acegi 和 JSF 的常见误解。然后提供一个简单的 web.xml 文件,可以用来部署 Acegi,从而保护 JSF 应用程序。然后深入探讨 Acegi 和 JSF 组件,了解在部署 web.xml 文件和用户访问 JSF 应用程序时所发生的事件。本文最后提供了一个由 Acegi 保护的示例 JSF 应用程序。

无需编写 Java 代码即可添加安全性

回顾一下本系列的第一个示例 Acegi 应用程序(请参阅 第 1 部分 中的 “一个简单 Acegi 应用程序” 一节)。该应用程序使用 Acegi 提供了以下安全特性:

  • 当一个未经验证的用户试图访问受保护的资源时,提供一个登录页面。
  • 将授权用户直接重定向到所需的受保护资源。
  • 如果用户未被授权访问受保护资源,提供一个访问拒绝页面。

回想一下,您无需编写任何 Java 代码就能获得这些特性。只需要对 Acegi 进行配置。同样,在 JSF 应用程序中,无需编写任何 Java 代码,也应该能够从 Acegi 实现相同的特性。

澄清误解

其他一些作者似乎认为将 Acegi 与 JSF 集成需要 JSF 应用程序提供登录页面(参见 参考资料)。这种观点并不正确。在需要时提供登录页面,这是 Acegi 的职责。确保登录页面在安全会话期间只出现一次,这也是 Acegi 的职责。然后,经过身份验证和授权的用户可以访问一个受保护资源,无需重复执行登录过程。

如果使用 JSF 提供登录页面,将会发生两个主要的问题:

  • 当需要时,没有利用 Acegi 的功能提供登录页面。必须编写 Java 代码实现所有逻辑来提供登录页面。

  • 至少需要编写一些 Java 代码将用户凭证(用户名和密码)从 JSF 的登录页面移交到 Acegi。

Acegi 的目的是避免编写 Java 安全代码。如果使用 JSF 提供登录页面,则没有实现这一用途,并且会引发一系列其他 JSF-Acegi 集成问题,所有这些问题都源于 “Acegi 是用来提供可配置安全性” 这一事实。如果试图使用 JSF 来完成 Acegi 的工作,将会遇到麻烦。

本文余下部分将解释并演示独立于 Acegi 的 JSF 应用程序开发,并在稍后配置 Acegi 以保护 JSF 应用程序 — 无需编写任何 Java 代码。首先看一下 web.xml 文件,可以部署该文件保护 JSF 应用程序。





回页首


部署 Acegi 保护 JSF 应用程序

清单 1 展示了一个 web.xml 文件(通常称为部署描述符),可以使用这个文件部署 Acegi,从而保护运行在 servlet 容器(比如 Apache Tomcat)中的 JSF 应用程序:


清单 1. 用于部署 Acegi 和 servlet 容器中的 JSF 的 web.xml 文件
                        <?xml version="1.0"?>
            <!DOCTYPE web-app PUBLIC
            "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
            "http://java.sun.com/dtd/web-app_2_3.dtd">
            <web-app>
            <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/acegi-config.xml</param-value>
            </context-param>
            <context-param>
            <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
            <param-value>server</param-value>
            </context-param>
            <context-param>
            <param-name>javax.faces.CONFIG_FILES</param-name>
            <param-value>/WEB-INF/faces-config.xml</param-value>
            </context-param>
            <listener>
            <listener-class>
            org.springframework.web.context.ContextLoaderListener
            </listener-class>
            </listener>
            <listener>
            <listener-class>
            com.sun.faces.config.ConfigureListener
            </listener-class>
            </listener>
            <!-- Faces Servlet -->
            <servlet>
            <servlet-name>Faces Servlet</servlet-name>
            <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
            <load-on-startup> 1 </load-on-startup>
            </servlet>
            <!-- Faces Servlet Mapping -->
            <servlet-mapping>
            <servlet-name>Faces Servlet</servlet-name>
            <url-pattern>*.faces</url-pattern>
            </servlet-mapping>
            <!-- Acegi filter configuration -->
            <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 Filter Mapping -->
            <filter-mapping>
            <filter-name>Acegi Filter Chain Proxy</filter-name>
            <url-pattern>/*</url-pattern>
            </filter-mapping>
            </web-app>
            

注意,清单 1 包含以下标记:

  • 3 个 <context-param> 标记
  • 2 个 <listener> 标记
  • 1 个 <filter> 标记
  • 1 个 <servlet> 标记
  • 1 个 <servlet-mapping> 标记
  • 1 个 <filter-mapping> 标记

阅读该文件,了解每个标记在 JSF-Acegi 应用程序中的用途。

向 Acegi 和 JSF 提供上下文参数

清单 1 中的每个 <context-param> 标记定义一个参数,供 Acegi 或 JSF 在启动或执行期间使用。第一个参数 — contextConfigLocation — 定义 Acegi 的 XML 配置文件的位置。

JSF 需要 javax.faces.STATE_SAVING_METHODjavax.faces.CONFIG_FILES 参数。javax.faces.STATE_SAVING_METHOD 参数指定希望在客户机还是服务器上存储 JSF 页面-视图状态。Sun 的参考实现的默认行为是将 JSF 视图存储在服务器上。

javax.faces.CONFIG_FILES 参数指定 JSF 需要的配置文件的位置。JSF 配置文件的详细信息不属于本文讨论的范围(参见 参考资料,获取涉及该主题的资源链接)。

为 Acegi 和 JSF 配置侦听器

现在看一下 清单 1 中的 2 个 <listener> 标记。<listener> 标记定义侦听器类,侦听器类侦听并处理 JSP 或 servlet 应用程序启动和执行期间发生的事件。例如:

  • 启动 JSP 或 servlet 应用程序时,servlet 容器创建一个新的 servlet 上下文。每当 JSP 或 servlet 应用程序启动时,就会触发此事件。

  • servlet 容器创建一个新的 servlet 请求对象。每当容器从客户机收到一个 HTTP 请求时,此事件就会发生。

  • 建立一个新的 HTTP 会话。当请求客户机建立一个与 servlet 容器的会话时,此事件就会发生。

  • 一个新属性被添加到 servlet 上下文、servlet 请求和 HTTP 会话对象。

  • servlet 上下文、servlet 请求或 HTTP 会话对象的一个现有属性被修改或删除。

<listener> 标记就像一种可扩展性机制,允许在 servlet 容器内部运行的应用程序协同某些事件进行处理。servlet 规范定义了侦听器类为处理事件而实现的一些接口。

例如,Spring Framework 实现一个 javax.servlet.ServletContextListener servlet 接口。实现此接口的 spring 类是 org.springframework.web.context.ContextLoaderListener。注意,这是 清单 1 的第一个 <listener> 标记中的侦听器类。

类似地,JSF 实现一个 com.sun.faces.config.ConfigureListener 类,该类实现一些事件-侦听接口。可以在 清单 1 的第二个 <listener> 标记中找到 ConfigureListener 类。

本文稍后将解释不同的事件-侦听器接口,以及 Acegi 和 JSF 事件-侦听器类内部执行的处理(请参阅 “启动 JSF-Acegi 应用程序” 和 “处理对受 Acegi 保护的 JSF 页面的请求”)。

配置和映射 servlet 过滤器

现在看一下 清单 1 中的 <filter> 标记。在请求的 servlet 处理传入的请求之前,servlet 应用程序使用过滤器对其进行预处理。在请求执行之前,Acegi 使用 servlet 过滤器对用户进行身份验证。

请注意 清单 1 中的 <filter> 标记,它的 <filter-class> 子标记指定一个 org.acegisecurity.util.FilterToBeanProxy 类。FilterToBeanProxy 类是 Acegi 的一部分。此类实现一个 javax.servlet.Filter 接口,该接口是 servlet 应用程序的一部分。javax.servlet.Filter 接口有一个 doFilter() 方法,servlet 容器在收到请求时调用该方法。

还需注意,清单 1<filter> 标记有另一个子标记 <init-param><init-param> 标记指定实例化 FilterToBeanProxy 类所需的参数。可以从 清单 1 中看出,FilterToBeanProxy 类只需要一个参数,该参数是 FilterChainProxy 类的一个对象。FilterChainProxy 类表示 第 1 部分 1 中讨论的整个 Acegi 过滤器链(请参阅 “安全过滤器” 小节)。FilterToBeanProxy 类的 doFilter() 方法使用 FilterChainProxy 类执行 Acegi 的安全过滤器链。

清单 1 中的 <filter-mapping> 标记指定调用 Acegi 的 FilterToBeanProxy 的请求 URL。我已经将所有的 JSF 页面映射到 Acegi 的 FilterToBeanProxy。这意味着只要用户试图访问 JSF 页面,FilterChainProxy doFilter() 方法就会自动获得控制权。

配置 JSF servlet

web.xml 文件中的 <servlet> 标记指定希望从特定 URl 调用的 servlet(在本例中是一个 JSF servlet)。<servlet-mapping> 标记定义该 URL。几乎所有的 JSP 或 servlet 应用程序都包含这两个标记,所以无需再作讨论(参见 参考资料,获取讨论 servlet 编程的资源链接)。

现在,您已经看到,web.xml 文件要部署 Acegi 以保护 JSF 应用程序所需的所有标记。您已经了解了侦听器、过滤器和 servlet 如何相互协作。从这里的讨论中可以看出,如果在 servlet 容器中部署 清单 1 中的 web.xml 文件,Acegi 和 JSF 都试图在两种情形下进行一些处理:

  • 当启动应用程序时
  • 当应用程序收到对 JSF 页面的请求时

接下来的两节解释每种情况中发生的一系列事件。





回页首


启动 JSF-Acegi 应用程序

图 1 展示了在 JSF-Acegi 应用程序启动时发生的事件顺序:


图 1. JSF-Acegi 应用程序启动时发生的事件顺序
JSF-Acegi 应用程序启动时发生的事件顺序


详细来讲,图 1 显示的事件顺序如下所示:

  1. servlet 容器实例化在 web.xml 文件中配置的所有侦听器。

  2. servlet 容器将 Acegi 的 ContextLoaderListener 注册为一个侦听器类,该类实现 javax.servlet.ServletContextListener接口。ServletContextListener 接口包含两个重要方法:contextInitialized()contextDestroyed()
    • contextInitialized() 方法在初始化 servlet 上下文时获得控制权。
    • 类似地,当应用程序退出时,contextDestroyed() 方法会被调用,并消除 servlet 上下文。

  3. servlet 容器将 JSF 的 ConfigureListener 注册为另一个侦听器。JSF 的 ConfigureListener 实现许多侦听器接口,比如 ServletContextListenerServletContextAttributeListenerServletRequestListener,以及 ServletRequestAttributeListener。您已经看到了 ServletContextListener 接口的方法。余下的接口是:
    • ServletContextAttributeListener,它包含 3 种方法:attributeAdded() attributeRemoved()attributeReplaced()。这 3 种方法分别在某个属性被添加到 servlet 上下文、被从 servlet 上下文删除、被新属性取代时获得控制权。attributeReplaced() 方法在 处理对受 Acegi 保护的 JSF 页面的请求 小节的第 8 步中获得控制权。

    • ServletRequestListener 中包含的方法在创建或删除新的 servlet 请求对象时获得控制权。servlet 请求方法表示并包装来自用户的请求。

    • ServletRequestAttributeListener 中包含的方法在添加、删除或替换某个请求对象的属性时获得控制权。本文稍后将讨论在 处理对受 Acegi 保护的 JSF 页面的请求 小节的第 3 步中创建一个新的请求对象时,JSF 的 ConfigureListener 执行的处理。

  4. servlet 容器创建一个 servlet 上下文对象,该对象封装应用程序资源(比如 JSP 页面、Java 类和应用程序初始化参数),并允许整个应用程序访问