針對 JavaServer Faces 應用程序的可配置安全性
本 系列 的前 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_METHOD 和 javax.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 應用程序都包含這兩個標記,所以無需 再作討論。
現在,您已經看到,web.xml 文件要部署 Acegi 以保護 JSF 應用程序所需 的所有標記。您已經了解了偵聽器、過濾器和 servlet 如何相互協作。從這裡 的討論中可以看出,如果在 servlet 容器中部署 清單 1 中的 web.xml 文件, Acegi 和 JSF 都試圖在兩種情形下進行一些處理:
當啟動應用程序時
當應用程序收到對 JSF 頁面的請求時
接下來的兩節解釋每種情況中發生的一系列事件。
啟動 JSF-Acegi 應用程序
圖 1 展示了在 JSF-Acegi 應用程序啟動時發生的事件順序:
圖 1. JSF-Acegi 應用程序啟動時發生的事件順序
詳細來講,圖 1 顯示的事件順序如下所示:
servlet 容器實例化在 web.xml 文件中配置的所有偵聽器。
servlet 容器將 Acegi 的 ContextLoaderListener 注冊為一個偵聽器類, 該類實現 javax.servlet.ServletContextListener接口。 ServletContextListener 接口包含兩個重要方法:contextInitialized() 和 contextDestroyed():
contextInitialized() 方法在初始化 servlet 上下文時獲得控制權。
類似地,當應用程序退出時,contextDestroyed() 方法會被調用,並消除 servlet 上下文。
servlet 容器將 JSF 的 ConfigureListener 注冊為另一個偵聽器。JSF 的 ConfigureListener 實現許多偵聽器接口,比如 ServletContextListener、 ServletContextAttributeListener、 ServletRequestListener,以及 ServletRequestAttributeListener。您已經看到了 ServletContextListener 接口的方法。余下的接口是:
ServletContextAttributeListener,它包含 3 種方法:attributeAdded() attributeRemoved() 和 attributeReplaced()。這 3 種方法分別在某個屬性被 添加到 servlet 上下文、被從 servlet 上下文刪除、被新屬性取代時獲得控制 權。attributeReplaced() 方法在 處理對受 Acegi 保護的 JSF 頁面的請求 小 節的第 8 步中獲得控制權。
ServletRequestListener 中包含的方法在創建或刪除新的 servlet 請求對 象時獲得控制權。servlet 請求方法表示並包裝來自用戶的請求。
ServletRequestAttributeListener 中包含的方法在添加、刪除或替換某個 請求對象的屬性時獲得控制權。本文稍後將討論在 處理對受 Acegi 保護的 JSF 頁面的請求 小節的第 3 步中創建一個新的請求對象時,JSF 的 ConfigureListener 執行的處理。
servlet 容器創建一個 servlet 上下文對象,該對象封裝應用程序資源(比 如 JSP 頁面、Java 類和應用程序初始化參數),並允許整個應用程序訪問這些 資源。JSF-Acegi 應用程序的所有其他組件(偵聽器、過濾器,以及 servlet) 在 servlet 上下文對象中以屬性的形式存儲與應用程序資源相關的信息。
servlet 容器通知 Acegi 的 ContextLoaderListener,servlet 上下文是通 過調用 ContextLoaderListener 的 contextInitializated() 方法初始化的。
contextInitialized() 方法解析 Acegi 的配置文件,為 JSF-Acegi 應用程 序創建 Web 應用程序上下文,以及實例化所有的安全過濾器和在 Acegi 配置文 件中配置的 Jave bean。在以後 JSF 應用程序收到來自客戶機的請求時,這些 過濾器對象將會用於身份驗證和授權(參閱 第 3 部分 中關於 Web 應用程序上 下文創建的討論和圖 1)。
servlet 容器通知 JSF 的 ConfigureListener,servlet 上下文是通過調用 contextInitialized() 方法初始化的。
contextInitialized() 方法檢查在 JSF 配置文件中配置的所有 JSF 托管 bean,確保 Java 類與每個 bean 並存。
servlet 容器檢查 web.xml 文件中任何配置的過濾器。例如,清單 1 中的 web.xml 文件包含一個 Acegi 過濾器 FilterToBeanProxy,servlet 容器將其 實例化、初始化並注冊為一個過濾器。Acegi 現在可以對傳入的請求執行身份驗 證和授權了。
servlet 容器實例化 faces servlet,後者開始偵聽從用戶傳入的請求。
下一節解釋 JSF-Acegi 應用程序收到來自用戶的請求時發生的一系列事件。
處理對受 Acegi 保護的 JSF 頁面的請求
您已經了解了如何配置 Acegi 保護 JSF 應用程序。也看到了當啟動 JSF- Acegi 應用程序時發生的一系列事件。本節描述當用戶發送一個對受 Acegi 保 護的 JSF 頁面的請求時,JSF 和 Acegi 組件如何在 servlet 容器的框架中運 行。
圖 2 展示了當客戶機發送一個對受 Acegi 保護的 JSF 頁面的請求時,發生 的事件順序:
圖 2. JSF 和 Acegi 協作提供 JSF 頁面
詳細來講,圖 2 展示的事件順序如下所示:
servlet 容器創建一個表示用戶請求的 servlet 請求對象。
回想一下 啟動 JSF-Acegi 應用程序 小節中的第 3 步,JSF 的 ConfigureListener 實現 ServletRequestListener 接口。這意味著 ConfigureListener 偵聽與創建和刪除 servlet 請求對象相關的事件。因此, servlet 容器調用 ConfigureListener 類的 requestInitialized() 方法。
requestInitialized() 方法准備執行請求的 JSF 生命周期。准備過程包括 檢查請求的 faces 上下文是否存在。faces 上下文封裝與應用程序資源相關的 信息。faces servlet 執行 JSF 生命周期時需要這些信息。如果此請求是新會 話的第一個請求,就會缺少 faces 上下文。在這種情況下, requestInitialized() 方法創建一個新的 faces 上下文。
servlet 容器檢查用戶的請求是否帶有任何狀態信息。如果 servlet 容器未 找到狀態信息,它會假設該請求是新會話的第一個請求,並為用戶創建一個 HTTP 會話對象。如果 servlet 容器發現該請求包含某種狀態信息(比如一個 cookie 或 URL 中的某種狀態信息),它就會根據保存的會話信息恢復用戶以前 的會話。
servlet 容器把請求 URL 與一個 URL 模式進行匹配,這個 URL 模式包含在 配置描述符中的 <filter-mapping> 標記的 <url-pattern> 子標 記中。如果請求 URL 與這個 URL 模式匹配,servlet 容器調用 Acegi 的 FilterToBeanProxy,FilterToBeanProxy 已在 圖 1 的第 9 步中被注冊為一個 servlet 過濾器。
Acegi 的 FilterToBeanProxy 使用 FilterChainProxy 類執行 Acegi 的完 整的安全過濾器鏈。Acegi 的過濾器自動檢查第 4 步中創建的 HTTP 會話對象 ,以查看請求客戶機是否已被驗證。如果 Acegi 發現用戶未被驗證,它提供一 個登錄頁面。否則,它就直接執行 第 2 部分 的 “配置攔截器” 一節中描述 的授權過程。
Acegi 使用經過驗證的用戶的會話信息更新 servlet 上下文。
servlet 容器通知 JSF 的 ConfigureListener 的 attributeReplaced() 方 法,servlet 上下文已被更新。ConfigureListener 檢查是否有任何 JSF bean 被更改。如果發現任何更改,它相應地更新 faces 上下文。但是,在本例中, 在身份驗證過程中 Acegi 沒有更改任何 JSF 托管 bean,因此在此調用期間 ConfigureListener 不進行任何處理。
如果授權過程成功,控制權被轉移到 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 的訪問。
本文配套源碼