注:本文是翻譯的javaworld.com上的一篇名為《Solving the logout problem properly and elegantly》的文章,原文請(qǐng)參看
Solving the logout problem properly and elegantly
文中所有示例程序的代碼可以從javaworld.com中下載。由于本人是Java初學(xué)者,水平有限,難免出現(xiàn)錯(cuò)誤,敬請(qǐng)各位拍磚,當(dāng)然,希望我花一個(gè)下午翻出來的文章能給您帶來幫助。
正確優(yōu)雅地解決用戶退出問題——JSP及Struts解決方案
摘要
???? 在一個(gè)有密碼保護(hù)的Web應(yīng)用中,正確處理用戶退出過程并不僅僅只需調(diào)用HttpSession的invalidate()方法。現(xiàn)在大部分瀏覽器上都有后退和前進(jìn)按鈕,允許用戶后退或前進(jìn)到一個(gè)頁面。如果在用戶在退出一個(gè)Web應(yīng)用后按了后退按鈕瀏覽器把緩存中的頁面呈現(xiàn)給用戶,這會(huì)使用戶產(chǎn)生疑惑,他們會(huì)開始擔(dān)心他們的個(gè)人數(shù)據(jù)是否安全。許多Web應(yīng)用強(qiáng)迫用戶退出時(shí)關(guān)閉整個(gè)瀏覽器,這樣,用戶就無法點(diǎn)擊后退按鈕了。還有一些使用JavaScript,但在某些客戶端瀏覽器這卻不一定起作用。這些解決方案都很笨拙且不能保證在任一情況下100%有效,同時(shí),它也要求用戶有一定的操作經(jīng)驗(yàn)。
????這篇文章以示例闡述了正確解決用戶退出問題的方案。作者Kevin Le首先描述了一個(gè)密碼保護(hù)Web應(yīng)用,然后以示例程序解釋問題如何產(chǎn)生并討論解決問題的方案。文章雖然是針對(duì)JSP頁面進(jìn)行闡述,但作者所闡述的概念很容易理解切能夠?yàn)槠渌鸚eb技術(shù)所采用。最后作者展示了如何用Jakarta Struts優(yōu)雅地解決這一問題。
??????大部分Web應(yīng)用不會(huì)包含象銀行賬戶或信用卡資料那樣機(jī)密的信息,但一旦涉及到敏感數(shù)據(jù),我們就需要提供一類密碼保護(hù)機(jī)制。舉例來說,一個(gè)工廠中工人通過Web訪問他們的時(shí)間安排、進(jìn)入他們的訓(xùn)練課程以及查看他們的薪金等等。此時(shí)應(yīng)用SSL(Secure Socket Layer)有點(diǎn)殺雞用牛刀的感覺,但不可否認(rèn),我們又必須為這些應(yīng)用提供密碼保護(hù),否則,工人(也就是Web應(yīng)用的使用者)可以窺探到工廠中其他雇員的私人機(jī)密信息。
????與上述情形相似的還有位處圖書館、醫(yī)院等公共場(chǎng)所的計(jì)算機(jī)。在這些地方,許多用戶共同使用幾臺(tái)計(jì)算機(jī),此時(shí)保護(hù)用戶的個(gè)人數(shù)據(jù)就顯得至關(guān)重要。設(shè)計(jì)良好編寫優(yōu)秀的應(yīng)用對(duì)用戶專業(yè)知識(shí)的要求少之又少。
????我們來看一下現(xiàn)實(shí)世界中一個(gè)完美的Web應(yīng)用是如何表現(xiàn)的:一個(gè)用戶通過瀏覽器訪問一個(gè)頁面。Web應(yīng)用展現(xiàn)一個(gè)登陸頁面要求用戶輸入有效的驗(yàn)證信息。用戶輸入了用戶名和密碼。此時(shí)我們假設(shè)用戶提供的身份驗(yàn)證信息是正確的,經(jīng)過了驗(yàn)證過程,Web應(yīng)用允許用戶瀏覽他有權(quán)訪問的區(qū)域。用戶想退出時(shí),點(diǎn)擊退出按鈕,Web應(yīng)用要求用戶確認(rèn)他是否則真的需要退出,如果用戶確定退出,Session結(jié)束,Web應(yīng)用重新定位到登陸頁面。用戶可以放心的離開而不用擔(dān)心他的信息會(huì)泄露。另一個(gè)用戶坐到了同一臺(tái)電腦前,他點(diǎn)擊后退按鈕,Web應(yīng)用不應(yīng)該出現(xiàn)上一個(gè)用戶訪問過的任何一個(gè)頁面。事實(shí)上,Web應(yīng)用在第二個(gè)用戶提供正確的驗(yàn)證信息之前應(yīng)當(dāng)一直停留在登陸頁面上。
????通過示例程序,文章向您闡述了如何在一個(gè)Web應(yīng)用中實(shí)現(xiàn)這一功能。
JSP示例
??????為了更為有效地闡述實(shí)現(xiàn)方案,本文將從展示一個(gè)示例應(yīng)用logoutSampleJSP1中碰到的問題開始。這個(gè)示例代表了許多沒有正確解決退出過程的Web應(yīng)用。logoutSampleJSP1包含了下述jsp頁面:login.jsp, home.jsp, secure1.jsp, secure2.jsp, logout.jsp, loginAction.jsp, and logoutAction.jsp。其中頁面home.jsp, secure1.jsp, secure2.jsp, 和logout.jsp是不允許未經(jīng)認(rèn)證的用戶訪問的,也就是說,這些頁面包含了重要信息,在用戶登陸之前或者退出之后都不應(yīng)該出現(xiàn)在瀏覽器中。login.jsp包含了用于用戶輸入用戶名和密碼的form。logout.jsp頁包含了要求用戶確認(rèn)是否退出的form。loginAction.jsp和logoutAction.jsp作為控制器分別包含了登陸和退出代碼。
????第二個(gè)示例應(yīng)用logoutSampleJSP2展示了如何解決示例logoutSampleJSP1中的問題。然而,第二個(gè)應(yīng)用自身也是有疑問的。在特定的情況下,退出問題還是會(huì)出現(xiàn)。
????第三個(gè)示例應(yīng)用logoutSampleJSP3在第二個(gè)示例上進(jìn)行了改進(jìn),比較完善地解決了退出問題。
????最后一個(gè)示例logoutSampleStruts展示了Struts如何優(yōu)美地解決登陸問題。
????注意:本文所附示例在最新版本的Microsoft Internet Explorer (IE), Netscape Navigator, Mozilla, FireFox和Avant瀏覽器上測(cè)試通過。
Login action
??????Brian Pontarelli的經(jīng)典文章《J2EE Security: Container Versus Custom》討論了不同的J2EE認(rèn)證途徑。文章同時(shí)指出,HTTP協(xié)議和基于form的認(rèn)證并未提供處理用戶退出的機(jī)制。因此,解決途徑便是引入自定義的安全實(shí)現(xiàn)機(jī)制。
????自定義的安全認(rèn)證機(jī)制普遍采用的方法是從form中獲得用戶輸入的認(rèn)證信息,然后到諸如LDAP (lightweight directory access protocol)或關(guān)系數(shù)據(jù)庫(kù)的安全域中進(jìn)行認(rèn)證。如果用戶提供的認(rèn)證信息是有效的,登陸動(dòng)作往HttpSession對(duì)象中注入某個(gè)對(duì)象。HttpSession存在著注入的對(duì)象則表示用戶已經(jīng)登陸。為了方便讀者理解,本文所附的示例只往HttpSession中寫入一個(gè)用戶名以表明用戶已經(jīng)登陸。清單1是從loginAction.jsp頁面中節(jié)選的一段代碼以此闡述登陸動(dòng)作:
????本文所附示例均以關(guān)系型數(shù)據(jù)庫(kù)作為安全域,但本文所闡述的觀點(diǎn)對(duì)任何類型的安全域都是適用的。
Logout action
??????退出動(dòng)作就包含了簡(jiǎn)單的刪除用戶名以及對(duì)用戶的HttpSession對(duì)象調(diào)用invalidate()方法。清單2是從loginoutAction.jsp頁面中節(jié)選的一段代碼以此闡述退出動(dòng)作:
阻止未經(jīng)認(rèn)證訪問受保護(hù)的JSP頁面
??????從form中獲取用戶提交的認(rèn)證信息并經(jīng)過驗(yàn)證后,登陸動(dòng)作簡(jiǎn)單地往 HttpSession對(duì)象中寫入一個(gè)用戶名,退出動(dòng)作則做相反的工作,它從用戶的HttpSession對(duì)象中刪除用戶名并調(diào)用invalidate()方法銷毀HttpSession。為了使登陸和退出動(dòng)作真正發(fā)揮作用,所有受保護(hù)的JSP頁面都應(yīng)該首先驗(yàn)證HttpSession中是否包含了用戶名以確認(rèn)當(dāng)前用戶是否已經(jīng)登陸。如果HttpSession中包含了用戶名,也就是說用戶已經(jīng)登陸,Web應(yīng)用則將剩余的JSP頁發(fā)送給瀏覽器,否則,JSP頁將跳轉(zhuǎn)到登陸頁login.jsp。頁面home.jsp, secure1.jsp, secure2.jsp和logout.jsp均包含清單3中的代碼段:
??????在這個(gè)代碼段中,程序從HttpSession中減縮username字符串。如果字符串為空,Web應(yīng)用則自動(dòng)中止執(zhí)行當(dāng)前頁面并跳轉(zhuǎn)到登陸頁,同時(shí)給出Session has ended. Please log in.的提示;如果不為空,Web應(yīng)用則繼續(xù)執(zhí)行,也就是把剩余的頁面提供給用戶。
運(yùn)行l(wèi)ogoutSampleJSP1
??????運(yùn)行l(wèi)ogoutSampleJSP1將會(huì)出現(xiàn)如下幾種情形:
????•??如果用戶沒有登陸,Web應(yīng)用將會(huì)正確中止受保護(hù)頁面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的執(zhí)行,也就是說,假如用戶在瀏覽器地址欄中直接敲入受保護(hù)JSP頁的地址試圖訪問,Web應(yīng)用將自動(dòng)跳轉(zhuǎn)到登陸頁并提示Session has ended.Please log in.
??????•??同樣的,當(dāng)一個(gè)用戶已經(jīng)退出,Web應(yīng)用也會(huì)正確中止受保護(hù)頁面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的執(zhí)行
????•??用戶退出后,如果點(diǎn)擊瀏覽器上的后退按鈕,Web應(yīng)用將不能正確保護(hù)受保護(hù)的頁面——在Session銷毀后(用戶退出)受保護(hù)的JSP頁重新在瀏覽器中顯示出來。然而,如果用戶點(diǎn)擊返回頁面上的任何鏈接,Web應(yīng)用將會(huì)跳轉(zhuǎn)到登陸頁面并提示Session has ended.Please log in.
阻止瀏覽器緩存
??????上述問題的根源在于大部分瀏覽器都有一個(gè)后退按鈕。當(dāng)點(diǎn)擊后退按鈕時(shí),默認(rèn)情況下瀏覽器不是從Web服務(wù)器上重新獲取頁面,而是從瀏覽器緩存中載入頁面。基于Java的Web應(yīng)用并未限制這一功能,在基于PHP、ASP和.NET的Web應(yīng)用中也同樣存在這一問題。
????在用戶點(diǎn)擊后退按鈕后,瀏覽器到服務(wù)器再?gòu)姆?wù)器到瀏覽器這樣通常意思上的HTTP回路并沒有建立,僅僅只是用戶,瀏覽器和緩存進(jìn)行了交互。所以,即使包含了清單3上的代碼來保護(hù)JSP頁面,當(dāng)點(diǎn)擊后退按鈕時(shí),這些代碼是不會(huì)執(zhí)行的。
????緩存的好壞,真是仁者見仁智者見智。緩存的確提供了一些便利,但通常只在使用靜態(tài)的HTML頁面或基于圖形或影響的頁面你才能感受到。而另一方面,Web應(yīng)用通常是基于數(shù)據(jù)的,數(shù)據(jù)通常是頻繁更改的。與從緩存中讀取并顯示過期的數(shù)據(jù)相比,提供最新的數(shù)據(jù)才是更重要的!
????幸運(yùn)的是,HTTP頭信息“Expires”和“Cache-Control”為應(yīng)用程序服務(wù)器提供了一個(gè)控制瀏覽器和代理服務(wù)器上緩存的機(jī)制。HTTP頭信息Expires告訴代理服務(wù)器它的緩存頁面何時(shí)將過期。HTTP1.1規(guī)范中新定義的頭信息Cache-Control可以通知瀏覽器不緩存任何頁面。當(dāng)點(diǎn)擊后退按鈕時(shí),瀏覽器重新訪問服務(wù)器已獲取頁面。如下是使用Cache-Control的基本方法:
????•??no-cache:強(qiáng)制緩存從服務(wù)器上獲取新的頁面
????•??no-store: 在任何環(huán)境下緩存不保存任何頁面
????HTTP1.0規(guī)范中的Pragma:no-cache等同于HTTP1.1規(guī)范中的Cache-Control:no-cache,同樣可以包含在頭信息中。
????通過使用HTTP頭信息的cache控制,第二個(gè)示例應(yīng)用logoutSampleJSP2解決了logoutSampleJSP1的問題。logoutSampleJSP2與logoutSampleJSP1不同表現(xiàn)在如下代碼段中,這一代碼段加入進(jìn)所有受保護(hù)的頁面中:
??????通過設(shè)置頭信息和檢查HttpSession中的用戶名確保了瀏覽器不緩存頁面,同時(shí),如果用戶未登陸,受保護(hù)的JSP頁面將不會(huì)發(fā)送到瀏覽器,取而代之的將是登陸頁面login.jsp。
運(yùn)行l(wèi)ogoutSampleJSP2
??????運(yùn)行l(wèi)ogoutSampleJSP2后將回看到如下結(jié)果:
????•??當(dāng)用戶退出后試圖點(diǎn)擊后退按鈕,瀏覽器并不會(huì)顯示受保護(hù)的頁面,它只會(huì)現(xiàn)實(shí)登陸頁login.jsp同時(shí)給出提示信息Session has ended. Please log in.
??????•??然而,當(dāng)按了后退按鈕返回的頁是處理用戶提交數(shù)據(jù)的頁面時(shí),IE和Avant瀏覽器將彈出如下信息提示:
?????????????? 警告:頁面已過期……(你肯定見過)
????選擇刷新后前一個(gè)JSP頁面將重新顯示在瀏覽器中。很顯然,這不是我們所想看到的因?yàn)樗`背了logout動(dòng)作的目的。發(fā)生這一現(xiàn)象時(shí),很可能是一個(gè)惡意用戶在嘗試獲取其他用戶的數(shù)據(jù)。然而,這個(gè)問題僅僅出現(xiàn)在后退按鈕對(duì)應(yīng)的是一個(gè)處理POST請(qǐng)求的頁面。
記錄最后登陸時(shí)間
??????上述問題之所以出現(xiàn)是因?yàn)闉g覽器將其緩存中的數(shù)據(jù)重新提交了。這本文的例子中,數(shù)據(jù)包含了用戶名和密碼。無論是否給出安全警告信息,瀏覽器此時(shí)起到了負(fù)面作用。
????為了解決logoutSampleJSP2中出現(xiàn)的問題,logoutSampleJSP3的login.jsp在包含username和password的基礎(chǔ)上還包含了一個(gè)稱作lastLogon的隱藏表單域,此表單域動(dòng)態(tài)的用一個(gè)long型值初始化。這個(gè)long型值是調(diào)用System.currentTimeMillis()獲取到的自1970年1月1日以來的毫秒數(shù)。當(dāng)login.jsp中的form提交時(shí),loginAction.jsp首先將隱藏域中的值與用戶數(shù)據(jù)庫(kù)中的值進(jìn)行比較。只有當(dāng)lastLogon表單域中的值大于數(shù)據(jù)庫(kù)中的值時(shí)Web應(yīng)用才認(rèn)為這是個(gè)有效的登陸。
????為了驗(yàn)證登陸,數(shù)據(jù)庫(kù)中l(wèi)astLogon字段必須以表單中的lastLogon值進(jìn)行更新。上例中,當(dāng)瀏覽器重復(fù)提交數(shù)據(jù)時(shí),表單中的lastLogon值不比數(shù)據(jù)庫(kù)中的lastLogon值大,因此,loginAction轉(zhuǎn)到login.jsp頁面,并提示Session has ended.Please log in.清單5是loginAction中節(jié)選的代碼段:
??????為了實(shí)現(xiàn)上述方法,你必須記錄每個(gè)用戶的最后登陸時(shí)間。對(duì)于采用關(guān)系型數(shù)據(jù)庫(kù)安全域來說,這點(diǎn)可以可以通過在某個(gè)表中加上lastLogin字段輕松實(shí)現(xiàn)。LDAP以及其他的安全域需要稍微動(dòng)下腦筋,但很顯然是可以實(shí)現(xiàn)的。
????表示最后登陸時(shí)間的方法有很多。示例logoutSampleJSP3利用了自1970年1月1日以來的毫秒數(shù)。這個(gè)方法在許多人在不同瀏覽器中用一個(gè)用戶賬號(hào)登陸時(shí)也是可行的。
運(yùn)行l(wèi)ogoutSampleJSP3
??????運(yùn)行示例logoutSampleJSP3將展示如何正確處理退出問題。一旦用戶退出,點(diǎn)擊瀏覽器上的后退按鈕在任何情況下都不會(huì)是受保護(hù)的頁面在瀏覽器上顯示出來。這個(gè)示例展示了如何正確處理退出問題而不需要額外的培訓(xùn)。
????為了使代碼更簡(jiǎn)練有效,一些冗余的代碼可以剔除掉。一種途徑就是把清單4中的代碼寫到一個(gè)單獨(dú)的JSP頁中,通過標(biāo)簽<jsp:include>其他頁面也可以引用。
Struts框架下的退出實(shí)現(xiàn)
??????與直接使用JSP或JSP/servlets相比,另一個(gè)可選的方案是使用Struts。為一個(gè)基于Struts的Web應(yīng)用添加一個(gè)處理退出問題的框架可以優(yōu)雅地不費(fèi)氣力的實(shí)現(xiàn)。這部分歸功于Struts是采用MVC設(shè)計(jì)模式的因此將模型和視圖清晰的分開。另外,Java是一個(gè)面向?qū)ο蟮恼Z言,其支持繼承,可以比JSP中的腳本更為容易地實(shí)現(xiàn)代碼重用。在Struts中,清單4中的代碼可以從JSP頁面中移植到Action類的execute()方法中。
????此外,我們還可以定義一個(gè)繼承Struts Action類的基本類,其execute()方法中包含了清單4中的代碼。通過使用類繼承機(jī)制,其他類可以繼承基本類中的通用邏輯來設(shè)置HTTP頭信息以及檢索HttpSession對(duì)象中的username字符串。這個(gè)基本類是一個(gè)抽象類并定義了一個(gè)抽象方法executeAction()。所有繼承自基類的子類都應(yīng)實(shí)現(xiàn)exectuteAction()方法而不是覆蓋它。清單6是基類的部分代碼:
??????清單6中的代碼與清單4中的很相像,僅僅只是用ActionMapping findForward替代了RequestDispatcher forward。清單6中,如果在HttpSession中未找到username字符串,ActionMapping對(duì)象將找到名為sessionEnded的forward元素并跳轉(zhuǎn)到對(duì)應(yīng)的path。如果找到了,子類將執(zhí)行其實(shí)現(xiàn)了executeAction()方法的業(yè)務(wù)邏輯。因此,在配置文件struts-web.xml中為所有子類聲明個(gè)一名為sessionEnded的forward元素是必須的。清單7以secure1 action闡明了這樣一個(gè)聲明:
????繼承自BaseAction類的子類Secure1Action實(shí)現(xiàn)了executeAction()方法而不是覆蓋它。Secure1Action類不執(zhí)行任何退出代碼,如清單8:
????只需要定義一個(gè)基類而不需要額外的代碼工作,上述解決方案是優(yōu)雅而有效的。不管怎樣,將通用的行為方法寫成一個(gè)繼承StrutsAction的基類是許多Struts項(xiàng)目的共同經(jīng)驗(yàn),值得推薦。
局限性
????上述解決方案對(duì)JSP或基于Struts的Web應(yīng)用都是非常簡(jiǎn)單而實(shí)用的,但它還是有某些局限。在我看來,這些局限并不是至關(guān)緊要的。(局限性未翻譯,請(qǐng)參見原文)
結(jié)論
????本文闡述了解決退出問題的方案,盡管方案簡(jiǎn)單的令人驚訝,但卻在所有情況下都能有效地工作。無論是對(duì)JSP還是Struts,所要做的不過是寫一段不超過50行的代碼以及一個(gè)記錄用戶最后登陸時(shí)間的方法。在Web應(yīng)用中混合使用這些方案能夠使擁護(hù)的私人數(shù)據(jù)不致泄露,同時(shí),也能增加用戶的經(jīng)驗(yàn)。
About the author
????Kevin H. Le has more than 12 years of experience in software development. In the first half of his career, his programming language of choice was C++. In 1997, he shifted his focus to Java. He has engaged in and successfully completed several J2EE and EAI projects as both developer and architect. In addition to J2EE, his current interests now include Web services and SOA. More information on Kevin can be found on his Website http://kevinhle.com.
正確優(yōu)雅地解決用戶退出問題——JSP及Struts解決方案
摘要
???? 在一個(gè)有密碼保護(hù)的Web應(yīng)用中,正確處理用戶退出過程并不僅僅只需調(diào)用HttpSession的invalidate()方法。現(xiàn)在大部分瀏覽器上都有后退和前進(jìn)按鈕,允許用戶后退或前進(jìn)到一個(gè)頁面。如果在用戶在退出一個(gè)Web應(yīng)用后按了后退按鈕瀏覽器把緩存中的頁面呈現(xiàn)給用戶,這會(huì)使用戶產(chǎn)生疑惑,他們會(huì)開始擔(dān)心他們的個(gè)人數(shù)據(jù)是否安全。許多Web應(yīng)用強(qiáng)迫用戶退出時(shí)關(guān)閉整個(gè)瀏覽器,這樣,用戶就無法點(diǎn)擊后退按鈕了。還有一些使用JavaScript,但在某些客戶端瀏覽器這卻不一定起作用。這些解決方案都很笨拙且不能保證在任一情況下100%有效,同時(shí),它也要求用戶有一定的操作經(jīng)驗(yàn)。
????這篇文章以示例闡述了正確解決用戶退出問題的方案。作者Kevin Le首先描述了一個(gè)密碼保護(hù)Web應(yīng)用,然后以示例程序解釋問題如何產(chǎn)生并討論解決問題的方案。文章雖然是針對(duì)JSP頁面進(jìn)行闡述,但作者所闡述的概念很容易理解切能夠?yàn)槠渌鸚eb技術(shù)所采用。最后作者展示了如何用Jakarta Struts優(yōu)雅地解決這一問題。
??????大部分Web應(yīng)用不會(huì)包含象銀行賬戶或信用卡資料那樣機(jī)密的信息,但一旦涉及到敏感數(shù)據(jù),我們就需要提供一類密碼保護(hù)機(jī)制。舉例來說,一個(gè)工廠中工人通過Web訪問他們的時(shí)間安排、進(jìn)入他們的訓(xùn)練課程以及查看他們的薪金等等。此時(shí)應(yīng)用SSL(Secure Socket Layer)有點(diǎn)殺雞用牛刀的感覺,但不可否認(rèn),我們又必須為這些應(yīng)用提供密碼保護(hù),否則,工人(也就是Web應(yīng)用的使用者)可以窺探到工廠中其他雇員的私人機(jī)密信息。
????與上述情形相似的還有位處圖書館、醫(yī)院等公共場(chǎng)所的計(jì)算機(jī)。在這些地方,許多用戶共同使用幾臺(tái)計(jì)算機(jī),此時(shí)保護(hù)用戶的個(gè)人數(shù)據(jù)就顯得至關(guān)重要。設(shè)計(jì)良好編寫優(yōu)秀的應(yīng)用對(duì)用戶專業(yè)知識(shí)的要求少之又少。
????我們來看一下現(xiàn)實(shí)世界中一個(gè)完美的Web應(yīng)用是如何表現(xiàn)的:一個(gè)用戶通過瀏覽器訪問一個(gè)頁面。Web應(yīng)用展現(xiàn)一個(gè)登陸頁面要求用戶輸入有效的驗(yàn)證信息。用戶輸入了用戶名和密碼。此時(shí)我們假設(shè)用戶提供的身份驗(yàn)證信息是正確的,經(jīng)過了驗(yàn)證過程,Web應(yīng)用允許用戶瀏覽他有權(quán)訪問的區(qū)域。用戶想退出時(shí),點(diǎn)擊退出按鈕,Web應(yīng)用要求用戶確認(rèn)他是否則真的需要退出,如果用戶確定退出,Session結(jié)束,Web應(yīng)用重新定位到登陸頁面。用戶可以放心的離開而不用擔(dān)心他的信息會(huì)泄露。另一個(gè)用戶坐到了同一臺(tái)電腦前,他點(diǎn)擊后退按鈕,Web應(yīng)用不應(yīng)該出現(xiàn)上一個(gè)用戶訪問過的任何一個(gè)頁面。事實(shí)上,Web應(yīng)用在第二個(gè)用戶提供正確的驗(yàn)證信息之前應(yīng)當(dāng)一直停留在登陸頁面上。
????通過示例程序,文章向您闡述了如何在一個(gè)Web應(yīng)用中實(shí)現(xiàn)這一功能。
JSP示例
??????為了更為有效地闡述實(shí)現(xiàn)方案,本文將從展示一個(gè)示例應(yīng)用logoutSampleJSP1中碰到的問題開始。這個(gè)示例代表了許多沒有正確解決退出過程的Web應(yīng)用。logoutSampleJSP1包含了下述jsp頁面:login.jsp, home.jsp, secure1.jsp, secure2.jsp, logout.jsp, loginAction.jsp, and logoutAction.jsp。其中頁面home.jsp, secure1.jsp, secure2.jsp, 和logout.jsp是不允許未經(jīng)認(rèn)證的用戶訪問的,也就是說,這些頁面包含了重要信息,在用戶登陸之前或者退出之后都不應(yīng)該出現(xiàn)在瀏覽器中。login.jsp包含了用于用戶輸入用戶名和密碼的form。logout.jsp頁包含了要求用戶確認(rèn)是否退出的form。loginAction.jsp和logoutAction.jsp作為控制器分別包含了登陸和退出代碼。
????第二個(gè)示例應(yīng)用logoutSampleJSP2展示了如何解決示例logoutSampleJSP1中的問題。然而,第二個(gè)應(yīng)用自身也是有疑問的。在特定的情況下,退出問題還是會(huì)出現(xiàn)。
????第三個(gè)示例應(yīng)用logoutSampleJSP3在第二個(gè)示例上進(jìn)行了改進(jìn),比較完善地解決了退出問題。
????最后一個(gè)示例logoutSampleStruts展示了Struts如何優(yōu)美地解決登陸問題。
????注意:本文所附示例在最新版本的Microsoft Internet Explorer (IE), Netscape Navigator, Mozilla, FireFox和Avant瀏覽器上測(cè)試通過。
Login action
??????Brian Pontarelli的經(jīng)典文章《J2EE Security: Container Versus Custom》討論了不同的J2EE認(rèn)證途徑。文章同時(shí)指出,HTTP協(xié)議和基于form的認(rèn)證并未提供處理用戶退出的機(jī)制。因此,解決途徑便是引入自定義的安全實(shí)現(xiàn)機(jī)制。
????自定義的安全認(rèn)證機(jī)制普遍采用的方法是從form中獲得用戶輸入的認(rèn)證信息,然后到諸如LDAP (lightweight directory access protocol)或關(guān)系數(shù)據(jù)庫(kù)的安全域中進(jìn)行認(rèn)證。如果用戶提供的認(rèn)證信息是有效的,登陸動(dòng)作往HttpSession對(duì)象中注入某個(gè)對(duì)象。HttpSession存在著注入的對(duì)象則表示用戶已經(jīng)登陸。為了方便讀者理解,本文所附的示例只往HttpSession中寫入一個(gè)用戶名以表明用戶已經(jīng)登陸。清單1是從loginAction.jsp頁面中節(jié)選的一段代碼以此闡述登陸動(dòng)作:
Listing 1
//...
//initialize RequestDispatcher object; set forward to home page by default
RequestDispatcher rd = request.getRequestDispatcher("home.jsp");
//Prepare connection and statement
rs = stmt.executeQuery("select password from USER where userName = '" + userName + "'");
if (rs.next()) { //Query only returns 1 record in the result set; only 1
??password per userName which is also the primary key
?? if (rs.getString("password").equals(password)) { //If valid password
??????session.setAttribute("User", userName); //Saves username string in the session object
?? }
?? else { //Password does not match, i.e., invalid user password
??????request.setAttribute("Error", "Invalid password.");
??????rd = request.getRequestDispatcher("login.jsp");
?? }
} //No record in the result set, i.e., invalid username
?? else {
??????request.setAttribute("Error", "Invalid user name.");
??????rd = request.getRequestDispatcher("login.jsp");
?? }
}
//As a controller, loginAction.jsp finally either forwards to "login.jsp" or "home.jsp"
rd.forward(request, response);
//...
????本文所附示例均以關(guān)系型數(shù)據(jù)庫(kù)作為安全域,但本文所闡述的觀點(diǎn)對(duì)任何類型的安全域都是適用的。
Logout action
??????退出動(dòng)作就包含了簡(jiǎn)單的刪除用戶名以及對(duì)用戶的HttpSession對(duì)象調(diào)用invalidate()方法。清單2是從loginoutAction.jsp頁面中節(jié)選的一段代碼以此闡述退出動(dòng)作:
Listing 2
//...
session.removeAttribute("User");
session.invalidate();
//...
阻止未經(jīng)認(rèn)證訪問受保護(hù)的JSP頁面
??????從form中獲取用戶提交的認(rèn)證信息并經(jīng)過驗(yàn)證后,登陸動(dòng)作簡(jiǎn)單地往 HttpSession對(duì)象中寫入一個(gè)用戶名,退出動(dòng)作則做相反的工作,它從用戶的HttpSession對(duì)象中刪除用戶名并調(diào)用invalidate()方法銷毀HttpSession。為了使登陸和退出動(dòng)作真正發(fā)揮作用,所有受保護(hù)的JSP頁面都應(yīng)該首先驗(yàn)證HttpSession中是否包含了用戶名以確認(rèn)當(dāng)前用戶是否已經(jīng)登陸。如果HttpSession中包含了用戶名,也就是說用戶已經(jīng)登陸,Web應(yīng)用則將剩余的JSP頁發(fā)送給瀏覽器,否則,JSP頁將跳轉(zhuǎn)到登陸頁login.jsp。頁面home.jsp, secure1.jsp, secure2.jsp和logout.jsp均包含清單3中的代碼段:
Listing 3
//...
String userName = (String) session.getAttribute("User");
if (null == userName) {
?? request.setAttribute("Error", "Session has ended.??Please login.");
?? RequestDispatcher rd = request.getRequestDispatcher("login.jsp");
?? rd.forward(request, response);
}
//...
//Allow the rest of the dynamic content in this JSP to be served to the browser
//...
??????在這個(gè)代碼段中,程序從HttpSession中減縮username字符串。如果字符串為空,Web應(yīng)用則自動(dòng)中止執(zhí)行當(dāng)前頁面并跳轉(zhuǎn)到登陸頁,同時(shí)給出Session has ended. Please log in.的提示;如果不為空,Web應(yīng)用則繼續(xù)執(zhí)行,也就是把剩余的頁面提供給用戶。
運(yùn)行l(wèi)ogoutSampleJSP1
??????運(yùn)行l(wèi)ogoutSampleJSP1將會(huì)出現(xiàn)如下幾種情形:
????•??如果用戶沒有登陸,Web應(yīng)用將會(huì)正確中止受保護(hù)頁面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的執(zhí)行,也就是說,假如用戶在瀏覽器地址欄中直接敲入受保護(hù)JSP頁的地址試圖訪問,Web應(yīng)用將自動(dòng)跳轉(zhuǎn)到登陸頁并提示Session has ended.Please log in.
??????•??同樣的,當(dāng)一個(gè)用戶已經(jīng)退出,Web應(yīng)用也會(huì)正確中止受保護(hù)頁面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的執(zhí)行
????•??用戶退出后,如果點(diǎn)擊瀏覽器上的后退按鈕,Web應(yīng)用將不能正確保護(hù)受保護(hù)的頁面——在Session銷毀后(用戶退出)受保護(hù)的JSP頁重新在瀏覽器中顯示出來。然而,如果用戶點(diǎn)擊返回頁面上的任何鏈接,Web應(yīng)用將會(huì)跳轉(zhuǎn)到登陸頁面并提示Session has ended.Please log in.
阻止瀏覽器緩存
??????上述問題的根源在于大部分瀏覽器都有一個(gè)后退按鈕。當(dāng)點(diǎn)擊后退按鈕時(shí),默認(rèn)情況下瀏覽器不是從Web服務(wù)器上重新獲取頁面,而是從瀏覽器緩存中載入頁面。基于Java的Web應(yīng)用并未限制這一功能,在基于PHP、ASP和.NET的Web應(yīng)用中也同樣存在這一問題。
????在用戶點(diǎn)擊后退按鈕后,瀏覽器到服務(wù)器再?gòu)姆?wù)器到瀏覽器這樣通常意思上的HTTP回路并沒有建立,僅僅只是用戶,瀏覽器和緩存進(jìn)行了交互。所以,即使包含了清單3上的代碼來保護(hù)JSP頁面,當(dāng)點(diǎn)擊后退按鈕時(shí),這些代碼是不會(huì)執(zhí)行的。
????緩存的好壞,真是仁者見仁智者見智。緩存的確提供了一些便利,但通常只在使用靜態(tài)的HTML頁面或基于圖形或影響的頁面你才能感受到。而另一方面,Web應(yīng)用通常是基于數(shù)據(jù)的,數(shù)據(jù)通常是頻繁更改的。與從緩存中讀取并顯示過期的數(shù)據(jù)相比,提供最新的數(shù)據(jù)才是更重要的!
????幸運(yùn)的是,HTTP頭信息“Expires”和“Cache-Control”為應(yīng)用程序服務(wù)器提供了一個(gè)控制瀏覽器和代理服務(wù)器上緩存的機(jī)制。HTTP頭信息Expires告訴代理服務(wù)器它的緩存頁面何時(shí)將過期。HTTP1.1規(guī)范中新定義的頭信息Cache-Control可以通知瀏覽器不緩存任何頁面。當(dāng)點(diǎn)擊后退按鈕時(shí),瀏覽器重新訪問服務(wù)器已獲取頁面。如下是使用Cache-Control的基本方法:
????•??no-cache:強(qiáng)制緩存從服務(wù)器上獲取新的頁面
????•??no-store: 在任何環(huán)境下緩存不保存任何頁面
????HTTP1.0規(guī)范中的Pragma:no-cache等同于HTTP1.1規(guī)范中的Cache-Control:no-cache,同樣可以包含在頭信息中。
????通過使用HTTP頭信息的cache控制,第二個(gè)示例應(yīng)用logoutSampleJSP2解決了logoutSampleJSP1的問題。logoutSampleJSP2與logoutSampleJSP1不同表現(xiàn)在如下代碼段中,這一代碼段加入進(jìn)所有受保護(hù)的頁面中:
//...
response.setHeader("Cache-Control","no-cache"); //Forces caches to obtain a new copy of the page from the origin server
response.setHeader("Cache-Control","no-store"); //Directs caches not to store the page under any circumstance
response.setDateHeader("Expires", 0); //Causes the proxy cache to see the page as "stale"
response.setHeader("Pragma","no-cache"); //HTTP 1.0 backward compatibility
String userName = (String) session.getAttribute("User");
if (null == userName) {
?? request.setAttribute("Error", "Session has ended.??Please login.");
?? RequestDispatcher rd = request.getRequestDispatcher("login.jsp");
?? rd.forward(request, response);
}
//...
??????通過設(shè)置頭信息和檢查HttpSession中的用戶名確保了瀏覽器不緩存頁面,同時(shí),如果用戶未登陸,受保護(hù)的JSP頁面將不會(huì)發(fā)送到瀏覽器,取而代之的將是登陸頁面login.jsp。
運(yùn)行l(wèi)ogoutSampleJSP2
??????運(yùn)行l(wèi)ogoutSampleJSP2后將回看到如下結(jié)果:
????•??當(dāng)用戶退出后試圖點(diǎn)擊后退按鈕,瀏覽器并不會(huì)顯示受保護(hù)的頁面,它只會(huì)現(xiàn)實(shí)登陸頁login.jsp同時(shí)給出提示信息Session has ended. Please log in.
??????•??然而,當(dāng)按了后退按鈕返回的頁是處理用戶提交數(shù)據(jù)的頁面時(shí),IE和Avant瀏覽器將彈出如下信息提示:
?????????????? 警告:頁面已過期……(你肯定見過)
????選擇刷新后前一個(gè)JSP頁面將重新顯示在瀏覽器中。很顯然,這不是我們所想看到的因?yàn)樗`背了logout動(dòng)作的目的。發(fā)生這一現(xiàn)象時(shí),很可能是一個(gè)惡意用戶在嘗試獲取其他用戶的數(shù)據(jù)。然而,這個(gè)問題僅僅出現(xiàn)在后退按鈕對(duì)應(yīng)的是一個(gè)處理POST請(qǐng)求的頁面。
記錄最后登陸時(shí)間
??????上述問題之所以出現(xiàn)是因?yàn)闉g覽器將其緩存中的數(shù)據(jù)重新提交了。這本文的例子中,數(shù)據(jù)包含了用戶名和密碼。無論是否給出安全警告信息,瀏覽器此時(shí)起到了負(fù)面作用。
????為了解決logoutSampleJSP2中出現(xiàn)的問題,logoutSampleJSP3的login.jsp在包含username和password的基礎(chǔ)上還包含了一個(gè)稱作lastLogon的隱藏表單域,此表單域動(dòng)態(tài)的用一個(gè)long型值初始化。這個(gè)long型值是調(diào)用System.currentTimeMillis()獲取到的自1970年1月1日以來的毫秒數(shù)。當(dāng)login.jsp中的form提交時(shí),loginAction.jsp首先將隱藏域中的值與用戶數(shù)據(jù)庫(kù)中的值進(jìn)行比較。只有當(dāng)lastLogon表單域中的值大于數(shù)據(jù)庫(kù)中的值時(shí)Web應(yīng)用才認(rèn)為這是個(gè)有效的登陸。
????為了驗(yàn)證登陸,數(shù)據(jù)庫(kù)中l(wèi)astLogon字段必須以表單中的lastLogon值進(jìn)行更新。上例中,當(dāng)瀏覽器重復(fù)提交數(shù)據(jù)時(shí),表單中的lastLogon值不比數(shù)據(jù)庫(kù)中的lastLogon值大,因此,loginAction轉(zhuǎn)到login.jsp頁面,并提示Session has ended.Please log in.清單5是loginAction中節(jié)選的代碼段:
清單5
//...
RequestDispatcher rd = request.getRequestDispatcher("home.jsp"); //Forward to homepage by default
//...
if (rs.getString("password").equals(password)) { //If valid password
?? long lastLogonDB = rs.getLong("lastLogon");
?? if (lastLogonForm > lastLogonDB) {
??????session.setAttribute("User", userName); //Saves username string in the session object
??????stmt.executeUpdate("update USER set lastLogon= " + lastLogonForm + " where userName = '" + userName + "'");
?? }
?? else {
??????request.setAttribute("Error", "Session has ended.??Please login.");
??????rd = request.getRequestDispatcher("login.jsp");????????}
}
else?? { //Password does not match, i.e., invalid user password
?? request.setAttribute("Error", "Invalid password.");
?? rd = request.getRequestDispatcher("login.jsp");??
}
//...
rd.forward(request, response);
//...
??????為了實(shí)現(xiàn)上述方法,你必須記錄每個(gè)用戶的最后登陸時(shí)間。對(duì)于采用關(guān)系型數(shù)據(jù)庫(kù)安全域來說,這點(diǎn)可以可以通過在某個(gè)表中加上lastLogin字段輕松實(shí)現(xiàn)。LDAP以及其他的安全域需要稍微動(dòng)下腦筋,但很顯然是可以實(shí)現(xiàn)的。
????表示最后登陸時(shí)間的方法有很多。示例logoutSampleJSP3利用了自1970年1月1日以來的毫秒數(shù)。這個(gè)方法在許多人在不同瀏覽器中用一個(gè)用戶賬號(hào)登陸時(shí)也是可行的。
運(yùn)行l(wèi)ogoutSampleJSP3
??????運(yùn)行示例logoutSampleJSP3將展示如何正確處理退出問題。一旦用戶退出,點(diǎn)擊瀏覽器上的后退按鈕在任何情況下都不會(huì)是受保護(hù)的頁面在瀏覽器上顯示出來。這個(gè)示例展示了如何正確處理退出問題而不需要額外的培訓(xùn)。
????為了使代碼更簡(jiǎn)練有效,一些冗余的代碼可以剔除掉。一種途徑就是把清單4中的代碼寫到一個(gè)單獨(dú)的JSP頁中,通過標(biāo)簽<jsp:include>其他頁面也可以引用。
Struts框架下的退出實(shí)現(xiàn)
??????與直接使用JSP或JSP/servlets相比,另一個(gè)可選的方案是使用Struts。為一個(gè)基于Struts的Web應(yīng)用添加一個(gè)處理退出問題的框架可以優(yōu)雅地不費(fèi)氣力的實(shí)現(xiàn)。這部分歸功于Struts是采用MVC設(shè)計(jì)模式的因此將模型和視圖清晰的分開。另外,Java是一個(gè)面向?qū)ο蟮恼Z言,其支持繼承,可以比JSP中的腳本更為容易地實(shí)現(xiàn)代碼重用。在Struts中,清單4中的代碼可以從JSP頁面中移植到Action類的execute()方法中。
????此外,我們還可以定義一個(gè)繼承Struts Action類的基本類,其execute()方法中包含了清單4中的代碼。通過使用類繼承機(jī)制,其他類可以繼承基本類中的通用邏輯來設(shè)置HTTP頭信息以及檢索HttpSession對(duì)象中的username字符串。這個(gè)基本類是一個(gè)抽象類并定義了一個(gè)抽象方法executeAction()。所有繼承自基類的子類都應(yīng)實(shí)現(xiàn)exectuteAction()方法而不是覆蓋它。清單6是基類的部分代碼:
清單6
??public abstract class BaseAction extends Action {
?? public ActionForward execute(ActionMapping mapping, ActionForm form,
??????HttpServletRequest request, HttpServletResponse response)
??????throws IOException, ServletException {
??????
??????response.setHeader("Cache-Control","no-cache"); //Forces caches to obtain a new copy of the page from the origin server
??????response.setHeader("Cache-Control","no-store"); //Directs caches not to store the page under any circumstance
??????response.setDateHeader("Expires", 0); //Causes the proxy cache to see the page as "stale"
??????response.setHeader("Pragma","no-cache"); //HTTP 1.0 backward compatibility
??????
??????if (!this.userIsLoggedIn(request)) {
???????? ActionErrors errors = new ActionErrors();
???????? errors.add("error", new ActionError("logon.sessionEnded"));
???????? this.saveErrors(request, errors);
???????? return mapping.findForward("sessionEnded");
??????}
??????return executeAction(mapping, form, request, response);
?? }
?? protected abstract ActionForward executeAction(ActionMapping mapping,
??????ActionForm form, HttpServletRequest request, HttpServletResponse response)
??????throws IOException, ServletException;??????
?? private boolean userIsLoggedIn(HttpServletRequest request) {
??????if (request.getSession().getAttribute("User") == null) {
???????? return false;
??????}
??????return true;
?? }
}
??????清單6中的代碼與清單4中的很相像,僅僅只是用ActionMapping findForward替代了RequestDispatcher forward。清單6中,如果在HttpSession中未找到username字符串,ActionMapping對(duì)象將找到名為sessionEnded的forward元素并跳轉(zhuǎn)到對(duì)應(yīng)的path。如果找到了,子類將執(zhí)行其實(shí)現(xiàn)了executeAction()方法的業(yè)務(wù)邏輯。因此,在配置文件struts-web.xml中為所有子類聲明個(gè)一名為sessionEnded的forward元素是必須的。清單7以secure1 action闡明了這樣一個(gè)聲明:
清單7
<action path="/secure1"
?? type="com.kevinhle.logoutSampleStruts.Secure1Action"??????????
?? scope="request">
?? <forward name="success" path="/WEB-INF/jsps/secure1.jsp"/>
?? <forward name="sessionEnded" path="/login.jsp"/>
</action>
????繼承自BaseAction類的子類Secure1Action實(shí)現(xiàn)了executeAction()方法而不是覆蓋它。Secure1Action類不執(zhí)行任何退出代碼,如清單8:
public class Secure1Action extends BaseAction {
?? public ActionForward executeAction(ActionMapping mapping, ActionForm form,
??????HttpServletRequest request, HttpServletResponse response)
??????throws IOException, ServletException {
??????
??????HttpSession session = request.getSession();????????????
??????return (mapping.findForward("success"));
?? }
}
????只需要定義一個(gè)基類而不需要額外的代碼工作,上述解決方案是優(yōu)雅而有效的。不管怎樣,將通用的行為方法寫成一個(gè)繼承StrutsAction的基類是許多Struts項(xiàng)目的共同經(jīng)驗(yàn),值得推薦。
局限性
????上述解決方案對(duì)JSP或基于Struts的Web應(yīng)用都是非常簡(jiǎn)單而實(shí)用的,但它還是有某些局限。在我看來,這些局限并不是至關(guān)緊要的。(局限性未翻譯,請(qǐng)參見原文)
結(jié)論
????本文闡述了解決退出問題的方案,盡管方案簡(jiǎn)單的令人驚訝,但卻在所有情況下都能有效地工作。無論是對(duì)JSP還是Struts,所要做的不過是寫一段不超過50行的代碼以及一個(gè)記錄用戶最后登陸時(shí)間的方法。在Web應(yīng)用中混合使用這些方案能夠使擁護(hù)的私人數(shù)據(jù)不致泄露,同時(shí),也能增加用戶的經(jīng)驗(yàn)。
About the author
????Kevin H. Le has more than 12 years of experience in software development. In the first half of his career, his programming language of choice was C++. In 1997, he shifted his focus to Java. He has engaged in and successfully completed several J2EE and EAI projects as both developer and architect. In addition to J2EE, his current interests now include Web services and SOA. More information on Kevin can be found on his Website http://kevinhle.com.