2008 年 3 月 20 日
Bilal Siddiqui 將繼續(xù)在他的 系列文章 中展示如何使用 Acegi 保護 Java™Server Faces (JSF) 應(yīng)用程序。配置 JSF 和 Acegi,讓它們在 servlet 容器中協(xié)作,探索 JSF 和 Acegi 組件如何彼此協(xié)作。
本 系列 的前 3 部分討論了如何使用 Acegi Security System 保護 Java 企業(yè)應(yīng)用程序:
- 第 1 部分 解釋了如何使用 Acegi 的內(nèi)置過濾器實現(xiàn)一個簡單的基于 URL 的安全系統(tǒng)。
- 第 2 部分 展示了如何編寫訪問控制策略、將其存儲在 LDAP 目錄服務(wù)器中,以及配置 Acegi 與 LDAP 服務(wù)器交互,從而實現(xiàn)訪問控制策略。
- 第 3 部分 展示了如何在企業(yè)應(yīng)用程序中使用 Acegi 保護對 Java 類實例的訪問。
第 4 部分將討論如何使用 Acegi 保護在 servlet 容器中運行的 JavaServer Faces (JSF) 應(yīng)用程序。本文首先解釋 Acegi 針對此目標(biāo)提供的特性,并澄清一些關(guān)于使用 Acegi 和 JSF 的常見誤解。然后提供一個簡單的 web.xml 文件,可以用來部署 Acegi,從而保護 JSF 應(yīng)用程序。然后深入探討 Acegi 和 JSF 組件,了解在部署 web.xml 文件和用戶訪問 JSF 應(yīng)用程序時所發(fā)生的事件。本文最后提供了一個由 Acegi 保護的示例 JSF 應(yīng)用程序。
無需編寫 Java 代碼即可添加安全性
回顧一下本系列的第一個示例 Acegi 應(yīng)用程序(請參閱 第 1 部分 中的 “一個簡單 Acegi 應(yīng)用程序” 一節(jié))。該應(yīng)用程序使用 Acegi 提供了以下安全特性:
- 當(dāng)一個未經(jīng)驗證的用戶試圖訪問受保護的資源時,提供一個登錄頁面。
- 將授權(quán)用戶直接重定向到所需的受保護資源。
- 如果用戶未被授權(quán)訪問受保護資源,提供一個訪問拒絕頁面。
回想一下,您無需編寫任何 Java 代碼就能獲得這些特性。只需要對 Acegi 進(jìn)行配置。同樣,在 JSF 應(yīng)用程序中,無需編寫任何 Java 代碼,也應(yīng)該能夠從 Acegi 實現(xiàn)相同的特性。
澄清誤解
其他一些作者似乎認(rèn)為將 Acegi 與 JSF 集成需要 JSF 應(yīng)用程序提供登錄頁面(參見 參考資料)。這種觀點并不正確。在需要時提供登錄頁面,這是 Acegi 的職責(zé)。確保登錄頁面在安全會話期間只出現(xiàn)一次,這也是 Acegi 的職責(zé)。然后,經(jīng)過身份驗證和授權(quán)的用戶可以訪問一個受保護資源,無需重復(fù)執(zhí)行登錄過程。
如果使用 JSF 提供登錄頁面,將會發(fā)生兩個主要的問題:
- 當(dāng)需要時,沒有利用 Acegi 的功能提供登錄頁面。必須編寫 Java 代碼實現(xiàn)所有邏輯來提供登錄頁面。
- 至少需要編寫一些 Java 代碼將用戶憑證(用戶名和密碼)從 JSF 的登錄頁面移交到 Acegi。
Acegi 的目的是避免編寫 Java 安全代碼。如果使用 JSF 提供登錄頁面,則沒有實現(xiàn)這一用途,并且會引發(fā)一系列其他 JSF-Acegi 集成問題,所有這些問題都源于 “Acegi 是用來提供可配置安全性” 這一事實。如果試圖使用 JSF 來完成 Acegi 的工作,將會遇到麻煩。
本文余下部分將解釋并演示獨立于 Acegi 的 JSF 應(yīng)用程序開發(fā),并在稍后配置 Acegi 以保護 JSF 應(yīng)用程序 — 無需編寫任何 Java 代碼。首先看一下 web.xml 文件,可以部署該文件保護 JSF 應(yīng)用程序。
部署 Acegi 保護 JSF 應(yīng)用程序
清單 1 展示了一個 web.xml 文件(通常稱為部署描述符),可以使用這個文件部署 Acegi,從而保護運行在 servlet 容器(比如 Apache Tomcat)中的 JSF 應(yīng)用程序:
清單 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 包含以下標(biāo)記:
- 3 個
<context-param>
標(biāo)記
- 2 個
<listener>
標(biāo)記
- 1 個
<filter>
標(biāo)記
- 1 個
<servlet>
標(biāo)記
- 1 個
<servlet-mapping>
標(biāo)記
- 1 個
<filter-mapping>
標(biāo)記
閱讀該文件,了解每個標(biāo)記在 JSF-Acegi 應(yīng)用程序中的用途。
向 Acegi 和 JSF 提供上下文參數(shù)
清單 1 中的每個 <context-param>
標(biāo)記定義一個參數(shù),供 Acegi 或 JSF 在啟動或執(zhí)行期間使用。第一個參數(shù) — contextConfigLocation
— 定義 Acegi 的 XML 配置文件的位置。
JSF 需要 javax.faces.STATE_SAVING_METHOD
和 javax.faces.CONFIG_FILES
參數(shù)。javax.faces.STATE_SAVING_METHOD
參數(shù)指定希望在客戶機還是服務(wù)器上存儲 JSF 頁面-視圖狀態(tài)。Sun 的參考實現(xiàn)的默認(rèn)行為是將 JSF 視圖存儲在服務(wù)器上。
javax.faces.CONFIG_FILES
參數(shù)指定 JSF 需要的配置文件的位置。JSF 配置文件的詳細(xì)信息不屬于本文討論的范圍(參見 參考資料,獲取涉及該主題的資源鏈接)。
為 Acegi 和 JSF 配置偵聽器
現(xiàn)在看一下 清單 1 中的 2 個 <listener>
標(biāo)記。<listener>
標(biāo)記定義偵聽器類,偵聽器類偵聽并處理 JSP 或 servlet 應(yīng)用程序啟動和執(zhí)行期間發(fā)生的事件。例如:
- 啟動 JSP 或 servlet 應(yīng)用程序時,servlet 容器創(chuàng)建一個新的 servlet 上下文。每當(dāng) JSP 或 servlet 應(yīng)用程序啟動時,就會觸發(fā)此事件。
- servlet 容器創(chuàng)建一個新的 servlet 請求對象。每當(dāng)容器從客戶機收到一個 HTTP 請求時,此事件就會發(fā)生。
- 建立一個新的 HTTP 會話。當(dāng)請求客戶機建立一個與 servlet 容器的會話時,此事件就會發(fā)生。
- 一個新屬性被添加到 servlet 上下文、servlet 請求和 HTTP 會話對象。
- servlet 上下文、servlet 請求或 HTTP 會話對象的一個現(xiàn)有屬性被修改或刪除。
<listener>
標(biāo)記就像一種可擴展性機制,允許在 servlet 容器內(nèi)部運行的應(yīng)用程序協(xié)同某些事件進(jìn)行處理。servlet 規(guī)范定義了偵聽器類為處理事件而實現(xiàn)的一些接口。
例如,Spring Framework 實現(xiàn)一個 javax.servlet.ServletContextListener
servlet 接口。實現(xiàn)此接口的 spring 類是 org.springframework.web.context.ContextLoaderListener
。注意,這是 清單 1 的第一個 <listener>
標(biāo)記中的偵聽器類。
類似地,JSF 實現(xiàn)一個 com.sun.faces.config.ConfigureListener
類,該類實現(xiàn)一些事件-偵聽接口??梢栽?清單 1 的第二個 <listener>
標(biāo)記中找到 ConfigureListener
類。
本文稍后將解釋不同的事件-偵聽器接口,以及 Acegi 和 JSF 事件-偵聽器類內(nèi)部執(zhí)行的處理(請參閱 “啟動 JSF-Acegi 應(yīng)用程序” 和 “處理對受 Acegi 保護的 JSF 頁面的請求”)。
配置和映射 servlet 過濾器
現(xiàn)在看一下 清單 1 中的 <filter>
標(biāo)記。在請求的 servlet 處理傳入的請求之前,servlet 應(yīng)用程序使用過濾器對其進(jìn)行預(yù)處理。在請求執(zhí)行之前,Acegi 使用 servlet 過濾器對用戶進(jìn)行身份驗證。
請注意 清單 1 中的 <filter>
標(biāo)記,它的 <filter-class>
子標(biāo)記指定一個 org.acegisecurity.util.FilterToBeanProxy
類。FilterToBeanProxy
類是 Acegi 的一部分。此類實現(xiàn)一個 javax.servlet.Filter
接口,該接口是 servlet 應(yīng)用程序的一部分。javax.servlet.Filter
接口有一個 doFilter()
方法,servlet 容器在收到請求時調(diào)用該方法。
還需注意,清單 1 的 <filter>
標(biāo)記有另一個子標(biāo)記 <init-param>
。<init-param>
標(biāo)記指定實例化 FilterToBeanProxy
類所需的參數(shù)??梢詮?清單 1 中看出,FilterToBeanProxy
類只需要一個參數(shù),該參數(shù)是 FilterChainProxy
類的一個對象。FilterChainProxy
類表示 第 1 部分 1 中討論的整個 Acegi 過濾器鏈(請參閱 “安全過濾器” 小節(jié))。FilterToBeanProxy
類的 doFilter()
方法使用 FilterChainProxy
類執(zhí)行 Acegi 的安全過濾器鏈。
清單 1 中的 <filter-mapping>
標(biāo)記指定調(diào)用 Acegi 的 FilterToBeanProxy
的請求 URL。我已經(jīng)將所有的 JSF 頁面映射到 Acegi 的 FilterToBeanProxy
。這意味著只要用戶試圖訪問 JSF 頁面,FilterChainProxy
doFilter()
方法就會自動獲得控制權(quán)。
配置 JSF servlet
web.xml 文件中的 <servlet>
標(biāo)記指定希望從特定 URl 調(diào)用的 servlet(在本例中是一個 JSF servlet)。<servlet-mapping>
標(biāo)記定義該 URL。幾乎所有的 JSP 或 servlet 應(yīng)用程序都包含這兩個標(biāo)記,所以無需再作討論(參見 參考資料,獲取討論 servlet 編程的資源鏈接)。
現(xiàn)在,您已經(jīng)看到,web.xml 文件要部署 Acegi 以保護 JSF 應(yīng)用程序所需的所有標(biāo)記。您已經(jīng)了解了偵聽器、過濾器和 servlet 如何相互協(xié)作。從這里的討論中可以看出,如果在 servlet 容器中部署 清單 1 中的 web.xml 文件,Acegi 和 JSF 都試圖在兩種情形下進(jìn)行一些處理:
- 當(dāng)啟動應(yīng)用程序時
- 當(dāng)應(yīng)用程序收到對 JSF 頁面的請求時
接下來的兩節(jié)解釋每種情況中發(fā)生的一系列事件。
啟動 JSF-Acegi 應(yīng)用程序
圖 1 展示了在 JSF-Acegi 應(yīng)用程序啟動時發(fā)生的事件順序:
圖 1. JSF-Acegi 應(yīng)用程序啟動時發(fā)生的事件順序
詳細(xì)來講,圖 1 顯示的事件順序如下所示:
- servlet 容器實例化在 web.xml 文件中配置的所有偵聽器。
- servlet 容器將 Acegi 的
ContextLoaderListener
注冊為一個偵聽器類,該類實現(xiàn) javax.servlet.ServletContextListener
接口。ServletContextListener
接口包含兩個重要方法:contextInitialized()
和 contextDestroyed()
:
contextInitialized()
方法在初始化 servlet 上下文時獲得控制權(quán)。
- 類似地,當(dāng)應(yīng)用程序退出時,
contextDestroyed()
方法會被調(diào)用,并消除 servlet 上下文。
- servlet 容器將 JSF 的
ConfigureListener
注冊為另一個偵聽器。JSF 的 ConfigureListener
實現(xiàn)許多偵聽器接口,比如 ServletContextListener
、 ServletContextAttributeListener
、 ServletRequestListener
,以及 ServletRequestAttributeListener
。您已經(jīng)看到了 ServletContextListener
接口的方法。余下的接口是:
ServletContextAttributeListener
,它包含 3 種方法:attributeAdded()
attributeRemoved()
和 attributeReplaced()
。這 3 種方法分別在某個屬性被添加到 servlet 上下文、被從 servlet 上下文刪除、被新屬性取代時獲得控制權(quán)。attributeReplaced()
方法在 處理對受 Acegi 保護的 JSF 頁面的請求 小節(jié)的第 8 步中獲得控制權(quán)。
ServletRequestListener
中包含的方法在創(chuàng)建或刪除新的 servlet 請求對象時獲得控制權(quán)。servlet 請求方法表示并包裝來自用戶的請求。
ServletRequestAttributeListener
中包含的方法在添加、刪除或替換某個請求對象的屬性時獲得控制權(quán)。本文稍后將討論在 處理對受 Acegi 保護的 JSF 頁面的請求 小節(jié)的第 3 步中創(chuàng)建一個新的請求對象時,JSF 的 ConfigureListener
執(zhí)行的處理。
- servlet 容器創(chuàng)建一個 servlet 上下文對象,該對象封裝應(yīng)用程序資源(比如 JSP 頁面、Java 類和應(yīng)用程序初始化參數(shù)),并允許整個應(yīng)用程序訪問這些資源。JSF-Acegi 應(yīng)用程序的所有其他組件(偵聽器、過濾器,以及 servlet)在 servlet 上下文對象中以屬性的形式存儲與應(yīng)用程序資源相關(guān)的信息。
- servlet 容器通知 Acegi 的
ContextLoaderListener
,servlet 上下文是通過調(diào)用 ContextLoaderListener
的 contextInitializated()
方法初始化的。
contextInitialized()
方法解析 Acegi 的配置文件,為 JSF-Acegi 應(yīng)用程序創(chuàng)建 Web 應(yīng)用程序上下文,以及實例化所有的安全過濾器和在 Acegi 配置文件中配置的 Jave bean。在以后 JSF 應(yīng)用程序收到來自客戶機的請求時,這些過濾器對象將會用于身份驗證和授權(quán)(參閱 第 3 部分 中關(guān)于 Web 應(yīng)用程序上下文創(chuàng)建的討論和圖 1)。
- servlet 容器通知 JSF 的
ConfigureListener
,servlet 上下文是通過調(diào)用 contextInitialized()
方法初始化的。
contextInitialized()
方法檢查在 JSF 配置文件中配置的所有 JSF 托管 bean,確保 Java 類與每個 bean 并存。
- servlet 容器檢查 web.xml 文件中任何配置的過濾器。例如,清單 1 中的 web.xml 文件包含一個 Acegi 過濾器
FilterToBeanProxy
,servlet 容器將其實例化、初始化并注冊為一個過濾器。Acegi 現(xiàn)在可以對傳入的請求執(zhí)行身份驗證和授權(quán)了。
- servlet 容器實例化 faces servlet,后者開始偵聽從用戶傳入的請求。
下一節(jié)解釋 JSF-Acegi 應(yīng)用程序收到來自用戶的請求時發(fā)生的一系列事件。
處理對受 Acegi 保護的 JSF 頁面的請求
您已經(jīng)了解了如何配置 Acegi 保護 JSF 應(yīng)用程序。也看到了當(dāng)啟動 JSF-Acegi 應(yīng)用程序時發(fā)生的一系列事件。本節(jié)描述當(dāng)用戶發(fā)送一個對受 Acegi 保護的 JSF 頁面的請求時,JSF 和 Acegi 組件如何在 servlet 容器的框架中運行。
圖 2 展示了當(dāng)客戶機發(fā)送一個對受 Acegi 保護的 JSF 頁面的請求時,發(fā)生的事件順序:
圖 2. JSF 和 Acegi 協(xié)作提供 JSF 頁面
詳細(xì)來講,圖 2 展示的事件順序如下所示:
- servlet 容器創(chuàng)建一個表示用戶請求的 servlet 請求對象。
- 回想一下 啟動 JSF-Acegi 應(yīng)用程序 小節(jié)中的第 3 步,JSF 的
ConfigureListener
實現(xiàn) ServletRequestListener
接口。這意味著 ConfigureListener
偵聽與創(chuàng)建和刪除 servlet 請求對象相關(guān)的事件。因此,servlet 容器調(diào)用 ConfigureListener
類的 requestInitialized()
方法。
requestInitialized()
方法準(zhǔn)備執(zhí)行請求的 JSF 生命周期。準(zhǔn)備過程包括檢查請求的 faces 上下文是否存在。faces 上下文封裝與應(yīng)用程序資源相關(guān)的信息。faces servlet 執(zhí)行 JSF 生命周期時需要這些信息。如果此請求是新會話的第一個請求,就會缺少 faces 上下文。在這種情況下,requestInitialized()
方法創(chuàng)建一個新的 faces 上下文。
- servlet 容器檢查用戶的請求是否帶有任何狀態(tài)信息。如果 servlet 容器未找到狀態(tài)信息,它會假設(shè)該請求是新會話的第一個請求,并為用戶創(chuàng)建一個 HTTP 會話對象。如果 servlet 容器發(fā)現(xiàn)該請求包含某種狀態(tài)信息(比如一個 cookie 或 URL 中的某種狀態(tài)信息),它就會根據(jù)保存的會話信息恢復(fù)用戶以前的會話。
- servlet 容器把請求 URL 與一個 URL 模式進(jìn)行匹配,這個 URL 模式包含在配置描述符中的
<filter-mapping>
標(biāo)記的 <url-pattern>
子標(biāo)記中。如果請求 URL 與這個 URL 模式匹配,servlet 容器調(diào)用 Acegi 的 FilterToBeanProxy
,FilterToBeanProxy
已在 圖 1 的第 9 步中被注冊為一個 servlet 過濾器。
- Acegi 的
FilterToBeanProxy
使用 FilterChainProxy
類執(zhí)行 Acegi 的完整的安全過濾器鏈。Acegi 的過濾器自動檢查第 4 步中創(chuàng)建的 HTTP 會話對象,以查看請求客戶機是否已被驗證。如果 Acegi 發(fā)現(xiàn)用戶未被驗證,它提供一個登錄頁面。否則,它就直接執(zhí)行 第 2 部分 的 “配置攔截器” 一節(jié)中描述的授權(quán)過程。
- Acegi 使用經(jīng)過驗證的用戶的會話信息更新 servlet 上下文。
- servlet 容器通知 JSF 的
ConfigureListener
的 attributeReplaced()
方法,servlet 上下文已被更新。ConfigureListener
檢查是否有任何 JSF bean 被更改。如果發(fā)現(xiàn)任何更改,它相應(yīng)地更新 faces 上下文。但是,在本例中,在身份驗證過程中 Acegi 沒有更改任何 JSF 托管 bean,因此在此調(diào)用期間 ConfigureListener
不進(jìn)行任何處理。
- 如果授權(quán)過程成功,控制權(quán)被轉(zhuǎn)移到 faces servlet,它執(zhí)行 JSF 生命周期并向用戶發(fā)回一個響應(yīng)。
現(xiàn)在,您了解了 JSF 和 Acegi 如何協(xié)作提供 JSF 請求,接下來看一下完成后的 JSF 和 Acegi。
示例 JSF-Acegi 應(yīng)用程序
本文的下載部分(參見 下載)包含一個示例 JSF-Acegi 應(yīng)用程序 JSFAcegiSample,演示了 Acegi 與 JSF 的簡單集成。示例應(yīng)用程序使用 清單 1 中的 web.xml。
要部署示例應(yīng)用程序,執(zhí)行 第 1 部分 的 “部署并運行應(yīng)用程序” 一節(jié)中的兩個步驟。還需要從 Sun 的 JSF 站點(參見 參考資料)下載并解壓 jsf-1_1_01.zip。將 jsf-1.1.X.zip 中的所有文件復(fù)制到 JSFAcegiSample 應(yīng)用程序的 WEB-INF/lib 文件夾中。
從瀏覽器訪問 http://localhost:8080/JSFAcegiSample,可以調(diào)用示例應(yīng)用程序。JSFAcegiSample 應(yīng)用程序顯示一個索引頁面和一個登錄頁面,索引頁面中包含受保護資源的鏈接。所有受保護頁面都是使用 JSF 組件開發(fā)的,而 Acegi 提供登錄頁面并執(zhí)行身份驗證和授權(quán)。
結(jié)束語
在本文中,了解了如何配置 Acegi 以保護 JSF 應(yīng)用程序。還詳細(xì)了解了 JSF 和 Acegi 組件如何在一個 servlet 容器的框架中協(xié)作。最后,嘗試運行了一個示例 JSF-Acegi 應(yīng)用程序。
關(guān)于實現(xiàn) JSF 應(yīng)用程序的 Acegi 安全性,還涉及到更多內(nèi)容。本系列的下一篇文章將演示如何使用 Acegi 保護對 JSF 的托管 bean 的訪問。
來自: http://www.cnblogs.com/amboyna/archive/2008/03/25/1122089.html