使用 Acegi 保护 Java 应用程序,第 4 部分: 保护 JSF 应用程序

针对 JavaServer Faces 应用程序的可配置安全性

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

Bilal Siddiqui (xml4java@yahoo.co.uk), 自由顾问, WaxSys

Bilal Siddiqui 是一名电子工程师、XML 顾问,他还是 WaxSys(主要从事电子商务简化)的创建者之一。自从 1995 年毕业于拉合尔工程技术大学(University of Engineering and Technology,Lahore)电子工程专业以后,他就开始为工业控制系统设计各种软件解决方案。稍后,他致力于 XML 方面并使用他在 C++ 编程中取得的经验来构建基于 Web 和 WAP 的 XML 处理工具、服务器端解析方案和服务应用程序。Bilal 是一名技术推广者,并且是一名多产的技术作家。



2008 年 3 月 20 日

系列 的前 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 页面,FilterChainProxydoFilter() 方法就会自动获得控制权。

配置 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 类和应用程序初始化参数),并允许整个应用程序访问这些资源。JSF-Acegi 应用程序的所有其他组件(侦听器、过滤器,以及 servlet)在 servlet 上下文对象中以属性的形式存储与应用程序资源相关的信息。
  5. servlet 容器通知 Acegi 的 ContextLoaderListener,servlet 上下文是通过调用 ContextLoaderListenercontextInitializated() 方法初始化的。
  6. contextInitialized() 方法解析 Acegi 的配置文件,为 JSF-Acegi 应用程序创建 Web 应用程序上下文,以及实例化所有的安全过滤器和在 Acegi 配置文件中配置的 Jave bean。在以后 JSF 应用程序收到来自客户机的请求时,这些过滤器对象将会用于身份验证和授权(参阅 第 3 部分 中关于 Web 应用程序上下文创建的讨论和图 1)。
  7. servlet 容器通知 JSF 的 ConfigureListener,servlet 上下文是通过调用 contextInitialized() 方法初始化的。
  8. contextInitialized() 方法检查在 JSF 配置文件中配置的所有 JSF 托管 bean,确保 Java 类 与每个 bean 并存。
  9. servlet 容器检查 web.xml 文件中任何配置的过滤器。例如,清单 1 中的 web.xml 文件包含一个 Acegi 过滤器 FilterToBeanProxy,servlet 容器将其实例化、初始化并注册为一个过滤器。Acegi 现在可以对传入的请求执行身份验证和授权了。
  10. servlet 容器实例化 faces servlet,后者开始侦听从用户传入的请求。

下一节解释 JSF-Acegi 应用程序收到来自用户的请求时发生的一系列事件。


处理对受 Acegi 保护的 JSF 页面的请求

您已经了解了如何配置 Acegi 保护 JSF 应用程序。也看到了当启动 JSF-Acegi 应用程序时发生的一系列事件。本节描述当用户发送一个对受 Acegi 保护的 JSF 页面的请求时,JSF 和 Acegi 组件如何在 servlet 容器的框架中运行。

图 2 展示了当客户机发送一个对受 Acegi 保护的 JSF 页面的请求时,发生的事件顺序:

图 2. JSF 和 Acegi 协作提供 JSF 页面
JSF 和 Acegi 协作提供 JSF 页面

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

  1. servlet 容器创建一个表示用户请求的 servlet 请求对象。
  2. 回想一下 启动 JSF-Acegi 应用程序 小节中的第 3 步,JSF 的 ConfigureListener 实现 ServletRequestListener 接口。这意味着 ConfigureListener 侦听与创建和删除 servlet 请求对象相关的事件。因此,servlet 容器调用 ConfigureListener 类的 requestInitialized() 方法。
  3. requestInitialized() 方法准备执行请求的 JSF 生命周期。准备过程包括检查请求的 faces 上下文是否存在。faces 上下文封装与应用程序资源相关的信息。faces servlet 执行 JSF 生命周期时需要这些信息。如果此请求是新会话的第一个请求,就会缺少 faces 上下文。在这种情况下,requestInitialized() 方法创建一个新的 faces 上下文。
  4. servlet 容器检查用户的请求是否带有任何状态信息。如果 servlet 容器未找到状态信息,它会假设该请求是新会话的第一个请求,并为用户创建一个 HTTP 会话对象。如果 servlet 容器发现该请求包含某种状态信息(比如一个 cookie 或 URL 中的某种状态信息),它就会根据保存的会话信息恢复用户以前的会话。
  5. servlet 容器把请求 URL 与一个 URL 模式进行匹配,这个 URL 模式包含在配置描述符中的 <filter-mapping> 标记的 <url-pattern> 子标记中。如果请求 URL 与这个 URL 模式匹配,servlet 容器调用 Acegi 的 FilterToBeanProxyFilterToBeanProxy 已在 图 1 的第 9 步中被注册为一个 servlet 过滤器。
  6. Acegi 的 FilterToBeanProxy 使用 FilterChainProxy 类执行 Acegi 的完整的安全过滤器链。Acegi 的过滤器自动检查第 4 步中创建的 HTTP 会话对象,以查看请求客户机是否已被验证。如果 Acegi 发现用户未被验证,它提供一个登录页面。否则,它就直接执行 第 2 部分 的 “配置拦截器” 一节中描述的授权过程。
  7. Acegi 使用经过验证的用户的会话信息更新 servlet 上下文。
  8. servlet 容器通知 JSF 的 ConfigureListenerattributeReplaced() 方法,servlet 上下文已被更新。ConfigureListener 检查是否有任何 JSF bean 被更改。如果发现任何更改,它相应地更新 faces 上下文。但是,在本例中,在身份验证过程中 Acegi 没有更改任何 JSF 托管 bean,因此在此调用期间 ConfigureListener 不进行任何处理。
  9. 如果授权过程成功,控制权被转移到 faces servlet,它执行 JSF 生命周期并向用户发回一个响应。

现在,您了解了 JSF 和 Acegi 如何协作提供 JSF 请求,接下来看一下完成后的 JSF 和 Acegi。


示例 JSF-Acegi 应用程序

本文的下载部分(参见 下载)包含一个示例 JSF-Acegi 应用程序 JSFAcegiSample,演示了 Acegi 与 JSF 的简单集成。示例应用程序使用 清单 1 中的 web.xml。

要部署示例应用程序,执行 第 1 部分 的 “部署并运行应用程序” 一节中的两个步骤。还需要从 Sun 的 JSF 站点(参见 参考资料)下载并解压 jsf-1_1_01.zip。将 jsf-1.1.X.zip 中的所有文件复制到 JSFAcegiSample 应用程序的 WEB-INF/lib 文件夹中。

从浏览器访问 http://localhost:8080/JSFAcegiSample,可以调用示例应用程序。JSFAcegiSample 应用程序显示一个索引页面和一个登录页面,索引页面中包含受保护资源的链接。所有受保护页面都是使用 JSF 组件开发的,而 Acegi 提供登录页面并执行身份验证和授权。


结束语

在本文中,了解了如何配置 Acegi 以保护 JSF 应用程序。还详细了解了 JSF 和 Acegi 组件如何在一个 servlet 容器的框架中协作。最后,尝试运行了一个示例 JSF-Acegi 应用程序。

关于实现 JSF 应用程序的 Acegi 安全性,还涉及到更多内容。本系列的下一篇文章将演示如何使用 Acegi 保护对 JSF 的托管 bean 的访问。


下载

描述名字大小
本文的源代码j-acegi4-source.zip15KB

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development
ArticleID=295852
ArticleTitle=使用 Acegi 保护 Java 应用程序,第 4 部分: 保护 JSF 应用程序
publish-date=03202008