W eb應用的前景是在不斷的演進中的,它已經從最開始作為共享文檔和信息的方式演化為業務管理的平臺,而在這種應用中,許可和 授權是一個關鍵的特征。Web的應用前景還在不斷的演進中,而本文把關注放在面向群體的應用中,例如博可和維基。
在這種應用中,由于作者希望有交流和反饋,所以授權不是非常嚴格的。有時,由于害怕識別,會造成失去一些人可能做出的貢獻。但是,缺少授權就會帶來諸如垃圾郵件這類的問題。這里有幾條在Web上抽取的信息:
很明顯,垃圾郵件發送源必須被識別出來。大多數者類惡意的攻擊發生數據匹配模式被識出來之后。一個可能的方法是人工而不是有計算機來停止這種攻擊,很顯然這是個挑戰。通過圖靈測試,按一個有名的計算機專家――阿蘭。圖靈命名的試驗,可以確定機器能夠實現類似人類操作的能力。者類測試中最有名的一個是CAPTCHA (an acronym for completely automated public Turing test to tell computers and humans apart)。這個測試常用來表明以個典型的情況:混亂或含義模糊的詞,人很容易識別,但對于光學識別軟件來講卻很困難。
圖1是一個典型的CAPTCHA.
圖1一個典型的CAPTCHA.
現在,大多數主要的服務提供商(Yahoo, Hotmail, Google)已經在他們的免費服務中使用CAPTCHA,用來作為區分垃圾郵件和虛假注冊的手段。在本文中,我們要描述以下在我們的Web應用中加入基于CAPTCHA授權的方法。
首先,我們快速瀏覽以下J2EE中Web應用的安全模型。
J2EE安全模型
在Java開發中,安全性始終是一個最受關注的領域。毫無疑問,J2EE在構建安全的應用時,采用了同樣的原理和健壯的框架。在J2EE中,安全性是一個很大的題目,在這里是不可能敘述細節的。在這方面有好多好的資源。我極力推薦團隊和個人花些時間來熟悉這些概念。在這力,我只能極概括的敘述一些最關鍵的概念。
關鍵概念
在J2EE應用中,安全性必須采用聲明或編程的話方法。就象名字中暗示的,當采用聲明方法時,開發者在應用軟件代碼的外部定義用于應用的安全性約束。這些聲明用部署描述符的形式來建造(web.xml, ejb-jar.xml),并由容器的運行環境來保證它的強制執行。聲明的方式容許開發者:
·能夠實現基于身份的對資源存取的約束(例如:/admin/* 只能容許有管理員身份的人來操作)
·能夠實現對某些URL的存取只能用某種協議的約束(例如:“/customer/*”只能通過HTTPS來訪問)
·能夠實現基于身份的對某些服務存取的約束(例如:可以限定SiteShutdownServlet只能由具有“god”身份的人來操作)
·能夠實現當用戶要存取某些受限資源但用戶還沒有登錄到系統的時候,自動重定向到登錄頁面的功能.而編程的方法能提供查詢和調用安全設施的機制,而開發者必須實現這些機制。這個方法的特點是:
·檢索出與當前用戶相關聯的部分:HttpServletRequest.getUserPrincipal or EJBContext.getCallerPrincipal
·查詢用戶是否具有某種特定的身份:HttpServletRequest.isUserInRole(String role) or EJBContext.isCallerInRole(String role)
這兩種方法都有它的局限性,并且是能互相補充的。
Web應用的聲明安全的方法
Web應用的聲明安全的方法本質上是一種被動的方法。這意味者只有在剛開始訪問受保護資源的用戶,如果他們沒有被受權,才會被重定向到登錄頁面。如果這個用戶已經被授權并有適當的權限,他們就能訪問這些資源。
這類方法中有一個最常用的方法是基于規則的受權。應用部署描述符web.xml分兩個部分描述了在這個方法中需要的所有元素。
第一部分是適用于整個應用的。它要鑒別出:
·在登錄中需要使用的方法。J2EE支持BASIC,DIGEST,FORM,或CERT等幾種授權機制。
·用于基于規則受權的登錄和錯誤的頁面
·能在應用中使用的所有身份的超集
圖2表明了第一部分的關鍵元素和它們之間的關系
圖2 第一部分的關鍵元素和它們之間的關系
第二部分說明了資源方面的約束。部署描述符可以包含零個或多個類似于下面的聲明:
·需要保護的站點。這可以在web-resource-collection內使用url-pattern來配置。
·能夠存取資源的身份的集合(auth-constraint)。它通常是第一部分定義的身份集合的一個子集。
·與某個資源相關的傳輸的保證(user-data-constraint)。
圖3表明了第二部分的關鍵元素和它們之間的關系
圖3 第二部分的關鍵元素和它們之間的關系
現在,我們看一個簡單的例子web.xml:
<web-app>
<!-- ... -->
<!--
Define the security constraint. This will limit the /admin/* portion of
the application to only be accessible to users within the "admin" role.
When an unauthenticated user attempts to access this section of the site,
they will be automatically presented with the login page.
-->
<security-constraint>
<!-- Define the context-relative URL(s) to be protected -->
<web-resource-collection>
<web-resource-name>Protected Area</web-resource-name>
<url-pattern>/admin/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<!-- Define the roles that are allowed to access this URL with the given methods -->
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
<!-- Transport guarantee could be used to guarantee an HTTPS protocol -->
<user-data-constraint>
<transport-guarantee>NONE</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!--
Define the method for authenticating a user when requesting a restricted
page. Methods include BASIC (the simple pop-up dialog), FORM and
CERTIFICATE.
-->
<login-config>
<!-- We will use form based authentication -->
<auth-method>FORM</auth-method>
<realm-name>Default Realm</realm-name>
<!-- where should the user be forwarded to enter his credentials -->
<form-login-config>
<form-login-page>/login/login.jsp</form-login-page>
<!--
On error the user will be shows this page It can also server side
forward back to the login page, which is popular behavior for most
sites.
-->
<form-error-page>/login/error.jsp</form-error-page>
</form-login-config>
</login-config>
<!--
Finally a list of all security roles in the application must be given.
-->
<security-role>
<description>Capable of administrating the site</description>
<role-name>admin</role-name>
</security-role>
</web-app>
這個簡單的部署描述符包含以下幾部分的安全配置:
·約束對有以/admin/*模式開頭的URLs的存取(URLl模式)
·在/admin下的資源只能使用HTTP GET或POST來存取(HTTP方法)
·資源能在標準的HTTP連接方式下提供服務(傳輸保證)
·只有具有管理員身份的用戶才能存取這些資源(身份命名)
·對遠程用戶使用基于規則的授權(授權方法)
·給用戶顯示一個登錄的頁面――/login/login.jsp ,以便用戶輸入信息來確認身份(形成登錄頁面)
·如果在授權過程中發生錯誤,給用戶顯示以個頁面來提示出錯――/login/error.jsp(形成出錯頁面)
擴展一個容器的安全設施
JAAS(Java Authentication and Authorization Services)實現了一個JAVA應用的可插入性授權模塊(PAM)。它容許平行開發安全部分和應用部分。開發者可以從這些選項中選擇并在應用中配置。由于容許與應用平行開發,所以JAAS有一些優點,還可以促進它在不同的應用中重用。
JAAS在應用的服務端也同樣有價值。然而,JAAS在J2EE中并沒有取得同樣的成功。直到最近,才開放了一些可定制的API用于擴展安全設施。但情況在改變。應用的服務端現在提供了適配器,可以容許把JAAS整合近已有的安全設施。這種整合仍然是與具體的應用相關的,并且非常復雜。
Tomcat提供了一種使用JAAS的相當簡單和直接整合的方法。用戶登錄模塊是用配置文件來配置的(Tomcat realm configuration and the standard JAAS configuration)。當服務端需要調用登錄模塊時,它把所有的請求都路由到org.apache.catalina.realm.JAASRealm適配器。把JAAS整合進Tomact的細節可參考相應的資料Resources。
在這篇文章,我們實現了一個JAAS模塊,并把它整合近Tomcat服務端,以提供J2EE的安全解決方案。
解決方法
在敘述實現方面之前,先描述一下解決方案的目標和所用的方法。
目標:
·為Web應用提供一種較弱的授權機制。這里,較弱的授權機制的含義是安全模塊或應用不區分用戶是否是在遠程訪問。
·不要求每個用戶在系統中有惟一的一個標識(登錄名)。這可以對遠程用戶做一定的隱藏。
·這種授權機制能區分計算機和用戶,這樣可以防止垃圾郵件的源自動登錄并濫用資源。我們用CAPTCHAs來做測試。
·這個授權機制應該基于J2EE的安全模型。我們要避免與這個模型不一致的方法。
根據以上的目標,很顯然,我們要保證每次會話中都是由實際的用戶參與的。應用的服務端管理會話來保持用戶的狀態。當一個未授權的用戶訪問受保護的資源的時候,J2EE的安全模塊要把用戶轉到登錄頁面。這個登錄頁面產生以個惟一的CAPTCHA并把它與用戶的會話聯系起來。這個登錄頁面就以一幅圖像的形式顯示這個CAPTCHA,并要求用戶識別這幅圖像。這個登錄頁面還有一個包含當前會話ID的隱藏起來的輸入域。
用戶把自己識別的結果添入輸入域并提交。在得到反饋后,負責登錄的模塊取出對話的ID和用戶的反饋。然后,把反饋與這個會話相關聯的CAPTCHA相比較。如果匹配,那么這個對這個用戶的鑒別就通過了,并把這個用戶的身份定為anonymous。
在授權后,遠端的用戶就可以以anonymous用戶的身份來存取所有受保護的資源。
圖4說明了我們的方法
圖4 我們的方法
實現
實現一個新授權機制的過程是相當明了的。整個過程可分為四個過程:
·保護Web資源
·為每個會話產生以個惟一的標識
·與已有的安全容器整合
·測試
下面詳細敘述以下這些過程。
保護Web資源
Web資源保護用J2EE聲明安全機制。下面的是web.xml文件的一個片斷,顯示了如何實現要求的配置。
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app>
<!-- constrain a section of the site -->
<security-constraint>
<display-name>Anonymous Security Constraint</display-name>
<web-resource-collection>
<web-resource-name>Protected Area</web-resource-name>
<url-pattern>/security/protected/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>anonymous</role-name>
</auth-constraint>
</security-constraint>
<!-- Default login configuration uses form-based authentication -->
<login-config>
<auth-method>FORM</auth-method>
<realm-name>Anonymous Form-Based Authentication Area</realm-name>
<form-login-config>
<form-login-page>/security/protected/login.jsp</form-login-page>
<form-error-page>/security/protected/error.jsp</form-error-page>
</form-login-config>
</login-config>
<!-- Security roles referenced by this web application -->
<security-role>
<role-name>anonymous</role-name>
</security-role>
<!-- The Usual Welcome File List -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>這個片斷包含了這樣一些內容:
·所有保存在/security/protected目錄下的資源(JSP網頁,SERVLETS)都要受容器的保護
·Anonymous是惟一的可訪問這些資源的用戶
·這個容器只接受HTTP 方式的GET 和POST請求
·受保護的資源只能通過常規的HTTP連接方式提供服務
·使用基于規則的授權;security/protected/login.jsp and /security/protected/error.jsp 分別作為登錄和出錯的頁面
在寫好安全部分后,就需要寫登錄和出錯的頁面。由于把每個會話與一個CAPTCHA或標識相關聯,所有login.jsp 包含到TokenServlet的請求。
login.jsp的一部分:
.....
.....
<%-- Generates and associates a CAPTCHA --%>
<img src="/servlet/AuthToken" alt="Your authentication token"/>
<%-- The form login page --%>
<form method="POST" action='<%= response.encodeURL("j_security_check") %>' >
<table border="0" cellspacing="5">
<input type="hidden" name="j_username" value="<%= session.getId() %>">
<tr>
<th align="right">Challenge:</th>
<td align="left"><input type="password" name="j_password"></td>
</tr>
<tr>
<td align="right"><input type="submit" value="Log In"></td>
<td align="left"><input type="reset"></td>
</tr>
</table>
</form>
.....
.....登錄頁面使用基于規則的安全方法,并把數據提交給j_security_check。j_username輸入域被隱藏起來,默認值是用戶會話的ID。另一個域,j_password是用戶輸入標識的地方。
出錯頁面是相當簡單的。它只是提示災害授權過程中發生樂和錯誤,還提供了以個到登錄頁面的連接。
產生新會話的惟一的標識
下面,需要提供與新會話相關聯的標識符的方法。
為支持產生多個標識,我們采用抽象工廠模式。jw.token.factory.TokenFactory是抽象工廠模式,根據給定的參數,初始化一個具體的標識符產生工廠,并將其返回。如果沒有要求一個特定的標識符產生工廠,就會返回一個默認的實現。Resources里有些分布式的可用的標識符產生工廠可以下載。第一個是我的業余練習jw.token.factory.SimpleTokenFactory。第二個是jw.token.Token,式由Jcaptcha項目提供的一個更成熟的實現。可以有getToken()來調用。標識符對象是一個用于CAPTCHAs的容器,可以通過在調用getTokenImage()時被當作一幅圖像來渲染。
圖5描述了標識符工廠對象模型
圖5 標識符工廠和標識符類的對象模型
一旦遠程客戶被定向到登錄頁,它要調用jw.jaas.servlet.TokenGeneratorServlet來取回以個標識符。TokenServlet的行為對于所有的標識符產生請求類似于看門人,它在web.xml中被配置。它接受一個初始化參數――tokenFactory,這個參數指明了要使用的標識符產生工廠。Servlet把請求委托給這個工廠,得到一個標識符,稍后把標識符作為一幅圖像傳給遠方的Web客戶端。
圖6說明了標識符產生的順序
圖6 標識符產生順序
為存儲與會話相關聯的標識符,我們創建以個緩沖。jw.token.AuthenticationTokenCache,它作為一個標識符的倉庫,還提供以個接口,以實現與某個會話相關的標識符的查詢、刪除,和新標識符的加入。緩沖是惟一的,并采用同步映射。
我們也需要周期的清理AuthenticationTokenCache。在web.xml中注冊jw.token.TokenInvalidationListener作為一個會話的監聽器。因此,不論何時,當一個會話被銷毀時,sessionDestroyed()總會被調用,然后標識符就從緩沖中被清除。
圖7顯示了服務模塊、緩沖和監聽器對象間的關系
圖7服務模塊、緩沖和監聽器對象間的關系
Web.xml顯示了servlet的配置
<web-app>
.....
.....
<listener>
<listener-class>jw.jaas.servlet.TokenInvalidationListener</listener-class>
</listener>
<!-- Standard Action Servlet Configuration (with debugging) -->
<servlet>
<servlet-name>tokengen</servlet-name>
<servlet-class>jw.jaas.servlet.TokenGeneratorServlet</servlet-class>
<load-on-startup>2</load-on-startup>
<init-param>
<param-name>tokenFactory</param-name>
<param-value>jw.token.factory.SimpleTokenFactory</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>tokengen</servlet-name>
<url-pattern>/servlet/AuthToken</url-pattern>
</servlet-mapping>
.....
.....
</web-app>
在部署描述符中,路徑/servlet/AuthToken被映射到TokenGeneratorServlet。Servlet被配置使用SimpleTokenFactory。
實現JAAS模塊
在JAAS Authentication Guide是說明實現JAAS登錄模塊的很好的參考資料。登錄模塊包含涉及生命周期的方法,如initialize(),login(),logout(),abort()和commit(),它有登錄上下文調用。大多數容器在它們的安全設施中提供插入JAAS登錄模塊的適配器。在Tomcat 中是JAASRealm。它截取登錄信息并產生一個回調句柄――JAASCallbackHandler,有能力處理兩種回調――NameCallback和PasswordCallback。這些回調分別包含在j_username 和 j_password輸入域中的值。然后,回調句柄被傳遞給登錄模塊來執行授權操作。
圖8顯示了登錄模塊類的對象模型
圖8登錄模塊類的對象模型
登錄模塊中的大多數的方法(commit(), abort(), initialize(), logout())是自解釋的。讓我們仔細看一下login()方法。
Login()方法從回調句柄中取得會話ID和用戶的回答。接下來,它會查詢AuthenticationTokenCache,以取得與這個會話相關聯的標識符。用戶的輸入會與標識符對象的內部狀態相比較。如果成功,用戶的會話會與一個很重要的調用AnonymousPrincipal相關聯。由于應用只對較弱的授權機制感興趣,所以你可以把這看做類似于把每個在你網站上的用戶命名為anonymous.
登錄上下文負責處理這個過程,并確保用戶已經登錄。用戶現在就已經登錄到系統,并有anonymous的身份。
圖9顯示了login()的順序圖
圖9 login()的順序圖
與容器的安全設施整合
現在考慮把各個部分與容器的安全設施相整合。這是一個與服務相關的步驟。Tomcat為每個Web 應用管理一個配置文件。這個文件命名為.xml,放在 /conf/Catalina/localhost文件夾下。把這個文件替換為下面的內容: %lt;?xml version='1.0' encoding='utf-8'?>
%lt;Context debug="9" docBase="d:/work/captcha-login/web" path="/clogin">
%lt;Realm className="org.apache.catalina.realm.JAASRealm" appName="clogin" debug="99" roleClassNames="jw.jaas.AnonymousPrincipal" userClassNames="jw.jaas.AnonymousPrincipal"/>
%lt;/Context>此外,我們要求JAAS配置直接與應用對應的登錄模塊。
JAAS配置文件:clogin {
jw.jaas.AnonymousLoginModule required debug=true;
};
為簡便起見,編譯腳本要自動產生所有的配置條目。
測試
為測試安全模塊,我建了一個Web應用,叫Anonymous Bulletin Board(ABB),這是一個容許用戶討論日常問題的應用,它的主頁要列出迄今所有發表的貼子。用戶也可以在上面發貼子。容許發貼子的頁是用生命安全的方式來保護的。只有通過授權的用戶才能發貼子。
圖10顯示了這種應用的一種屏幕顯示。
圖10這種應用的一種屏幕顯示
必要的準備
為了構造和測試這個應用,我們需要Web container version 5.0或更高的版本,還有Ant build environment version 1.5或更高的版本。這些可以從Apache下載和安裝。一旦有了這些準備,我們就可以安下面的步驟來構造和部署應用了。
安裝和測試
·下載并解壓縮jw-0307-captcha.zip到一個目錄(如:d:\captcha)。解壓縮出來的文件會包括源代碼、庫文件、Web應用和編譯腳本。
·轉到文件被解壓縮到的目錄,編輯文件setAntEnv.cmd。象文件名暗示的那樣,這個文件包含開發環境啟動時所需要的變量。編輯變量ANT_HOME和JAVA_HOME,使其指向正確的位置。所有其它的部分都是從這兩個變量導出的,因此不需要改動。
·還是在這個目錄下,編輯文件ant.developer.properties。這個文件包含影響應用編譯采用的方法的部分。文件中的大部分都有一個有意義的默認值,你只需要設置build.home和server.home。
·現在,打開命令行窗口,并轉到這個目錄(d:\captcha),并執行setAntEnv.cmd批處理腳本。
·輸入ant compile jar deploy來編譯并部署這個應用。Ant腳本首先創建需要的目錄,編譯并安全模塊代碼,并打包為JAR文件。最后部署它。
·通過輸入ant start命令來啟動應用的服務端。Tomcat應該在以個獨立的窗口啟動并運行。
·打開瀏覽器,讓它指向http://localhost:8080/clogin (clogin是在ant.developer.properties 中配置的application.name屬性)。
·在測試完畢后,你可以通過關閉Tomact窗口的方式來停止服務。
無論什么時候改變了ant.developer.properties中的屬性,都要重新編譯,以保證所有的屬性的依賴都是正確的。例如,如果你使用了out-of-box配置,標示符服務器將使用簡單的標示符工廠。你也可以把文件ant.developer.properties中的captcha.token.factory的值改為jw.token.factory.JCaptchaTokenFactory來做更復雜的測試。運行ant clean-all deploy來傳播變化。啟動服務端并測試應用。
調試
根據你的環境的不同,應用在啟動的時候可能會失敗。編譯腳本已經部署了一個用于Tomcat配置的的名為log4j的文件。你可以打開/logs/jaasModule.log或 /logs/tomcat.log,看一下追蹤和調試級的信息。jaasModule.log包含了登錄模塊和Web應用的調試信息。tomcat.log捕捉了從容器和部署的代碼產生的登錄消息。打開這些文件來尋找線索。
結論
在這篇文章中,我們瀏覽了J2EE Web應用的聲明安全模型,還有擴展它來使用JAAS登錄模塊的方法。我們擴展了Tomcat的J2EE安全設施以支持一種用戶定義的授權機制。這個授權機制是作為一個JAAS登錄模塊實現的,并在較弱的用戶授權中使用了CAPTCHSs。對于某種應用領域,它能確定遠端用戶是一個人。使用容器提供的安全機制,我們可以很容易的確保應用的安全而不用更改任何代碼。
應該注意到,CAPTCHAa既可以產生對用戶有用的結果,也可以產生無用的結果。當開發者在選擇授權機制的時候應該注意到這個問題。
關于作者
Anand Raman作為技術方面的合作者為Sapient在德里以外的地方工作。在過去的五年中一直從事JAVA和J2EE相關的技術工作。追蹤J2EE的復雜性是他最感興趣的。