<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    空間站

    北極心空

      BlogJava :: 首頁 :: 聯系 :: 聚合  :: 管理
      15 Posts :: 393 Stories :: 160 Comments :: 0 Trackbacks
    注:本文是翻譯的javaworld.com上的一篇名為《Solving the logout problem properly and elegantly》的文章,原文請參看 Solving the logout problem properly and elegantly 文中所有示例程序的代碼可以從javaworld.com中下載。由于本人是Java初學者,水平有限,難免出現錯誤,敬請各位拍磚,當然,希望我花一個下午翻出來的文章能給您帶來幫助。

    正確優雅地解決用戶退出問題——JSP及Struts解決方案

    摘要

    ???? 在一個有密碼保護的Web應用中,正確處理用戶退出過程并不僅僅只需調用HttpSession的invalidate()方法。現在大部分瀏覽器上都有后退和前進按鈕,允許用戶后退或前進到一個頁面。如果在用戶在退出一個Web應用后按了后退按鈕瀏覽器把緩存中的頁面呈現給用戶,這會使用戶產生疑惑,他們會開始擔心他們的個人數據是否安全。許多Web應用強迫用戶退出時關閉整個瀏覽器,這樣,用戶就無法點擊后退按鈕了。還有一些使用JavaScript,但在某些客戶端瀏覽器這卻不一定起作用。這些解決方案都很笨拙且不能保證在任一情況下100%有效,同時,它也要求用戶有一定的操作經驗。
    ????這篇文章以示例闡述了正確解決用戶退出問題的方案。作者Kevin Le首先描述了一個密碼保護Web應用,然后以示例程序解釋問題如何產生并討論解決問題的方案。文章雖然是針對JSP頁面進行闡述,但作者所闡述的概念很容易理解切能夠為其他Web技術所采用。最后作者展示了如何用Jakarta Struts優雅地解決這一問題。



    ??????大部分Web應用不會包含象銀行賬戶或信用卡資料那樣機密的信息,但一旦涉及到敏感數據,我們就需要提供一類密碼保護機制。舉例來說,一個工廠中工人通過Web訪問他們的時間安排、進入他們的訓練課程以及查看他們的薪金等等。此時應用SSL(Secure Socket Layer)有點殺雞用牛刀的感覺,但不可否認,我們又必須為這些應用提供密碼保護,否則,工人(也就是Web應用的使用者)可以窺探到工廠中其他雇員的私人機密信息。
    ????與上述情形相似的還有位處圖書館、醫院等公共場所的計算機。在這些地方,許多用戶共同使用幾臺計算機,此時保護用戶的個人數據就顯得至關重要。設計良好編寫優秀的應用對用戶專業知識的要求少之又少。
    ????我們來看一下現實世界中一個完美的Web應用是如何表現的:一個用戶通過瀏覽器訪問一個頁面。Web應用展現一個登陸頁面要求用戶輸入有效的驗證信息。用戶輸入了用戶名和密碼。此時我們假設用戶提供的身份驗證信息是正確的,經過了驗證過程,Web應用允許用戶瀏覽他有權訪問的區域。用戶想退出時,點擊退出按鈕,Web應用要求用戶確認他是否則真的需要退出,如果用戶確定退出,Session結束,Web應用重新定位到登陸頁面。用戶可以放心的離開而不用擔心他的信息會泄露。另一個用戶坐到了同一臺電腦前,他點擊后退按鈕,Web應用不應該出現上一個用戶訪問過的任何一個頁面。事實上,Web應用在第二個用戶提供正確的驗證信息之前應當一直停留在登陸頁面上。
    ????通過示例程序,文章向您闡述了如何在一個Web應用中實現這一功能。



    JSP示例
    ??????為了更為有效地闡述實現方案,本文將從展示一個示例應用logoutSampleJSP1中碰到的問題開始。這個示例代表了許多沒有正確解決退出過程的Web應用。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是不允許未經認證的用戶訪問的,也就是說,這些頁面包含了重要信息,在用戶登陸之前或者退出之后都不應該出現在瀏覽器中。login.jsp包含了用于用戶輸入用戶名和密碼的form。logout.jsp頁包含了要求用戶確認是否退出的form。loginAction.jsp和logoutAction.jsp作為控制器分別包含了登陸和退出代碼。
    ????第二個示例應用logoutSampleJSP2展示了如何解決示例logoutSampleJSP1中的問題。然而,第二個應用自身也是有疑問的。在特定的情況下,退出問題還是會出現。
    ????第三個示例應用logoutSampleJSP3在第二個示例上進行了改進,比較完善地解決了退出問題。
    ????最后一個示例logoutSampleStruts展示了Struts如何優美地解決登陸問題。
    ????注意:本文所附示例在最新版本的Microsoft Internet Explorer (IE), Netscape Navigator, Mozilla, FireFox和Avant瀏覽器上測試通過。


    Login action
    ??????Brian Pontarelli的經典文章《J2EE Security: Container Versus Custom》討論了不同的J2EE認證途徑。文章同時指出,HTTP協議和基于form的認證并未提供處理用戶退出的機制。因此,解決途徑便是引入自定義的安全實現機制。
    ????自定義的安全認證機制普遍采用的方法是從form中獲得用戶輸入的認證信息,然后到諸如LDAP (lightweight directory access protocol)或關系數據庫的安全域中進行認證。如果用戶提供的認證信息是有效的,登陸動作往HttpSession對象中注入某個對象。HttpSession存在著注入的對象則表示用戶已經登陸。為了方便讀者理解,本文所附的示例只往HttpSession中寫入一個用戶名以表明用戶已經登陸。清單1是從loginAction.jsp頁面中節選的一段代碼以此闡述登陸動作:


    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 = &#39;" + userName + "&#39;");
    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);
    //...



    ????本文所附示例均以關系型數據庫作為安全域,但本文所闡述的觀點對任何類型的安全域都是適用的。

    Logout action
    ??????退出動作就包含了簡單的刪除用戶名以及對用戶的HttpSession對象調用invalidate()方法。清單2是從loginoutAction.jsp頁面中節選的一段代碼以此闡述退出動作:


    Listing 2
    //...
    session.removeAttribute("User");
    session.invalidate();
    //...



    阻止未經認證訪問受保護的JSP頁面
    ??????從form中獲取用戶提交的認證信息并經過驗證后,登陸動作簡單地往 HttpSession對象中寫入一個用戶名,退出動作則做相反的工作,它從用戶的HttpSession對象中刪除用戶名并調用invalidate()方法銷毀HttpSession。為了使登陸和退出動作真正發揮作用,所有受保護的JSP頁面都應該首先驗證HttpSession中是否包含了用戶名以確認當前用戶是否已經登陸。如果HttpSession中包含了用戶名,也就是說用戶已經登陸,Web應用則將剩余的JSP頁發送給瀏覽器,否則,JSP頁將跳轉到登陸頁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
    //...



    ??????在這個代碼段中,程序從HttpSession中減縮username字符串。如果字符串為空,Web應用則自動中止執行當前頁面并跳轉到登陸頁,同時給出Session has ended. Please log in.的提示;如果不為空,Web應用則繼續執行,也就是把剩余的頁面提供給用戶。

    運行logoutSampleJSP1
    ??????運行logoutSampleJSP1將會出現如下幾種情形:
    ????&#8226;??如果用戶沒有登陸,Web應用將會正確中止受保護頁面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的執行,也就是說,假如用戶在瀏覽器地址欄中直接敲入受保護JSP頁的地址試圖訪問,Web應用將自動跳轉到登陸頁并提示Session has ended.Please log in.
    ??????&#8226;??同樣的,當一個用戶已經退出,Web應用也會正確中止受保護頁面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的執行
    ????&#8226;??用戶退出后,如果點擊瀏覽器上的后退按鈕,Web應用將不能正確保護受保護的頁面——在Session銷毀后(用戶退出)受保護的JSP頁重新在瀏覽器中顯示出來。然而,如果用戶點擊返回頁面上的任何鏈接,Web應用將會跳轉到登陸頁面并提示Session has ended.Please log in.

    阻止瀏覽器緩存
    ??????上述問題的根源在于大部分瀏覽器都有一個后退按鈕。當點擊后退按鈕時,默認情況下瀏覽器不是從Web服務器上重新獲取頁面,而是從瀏覽器緩存中載入頁面。基于Java的Web應用并未限制這一功能,在基于PHP、ASP和.NET的Web應用中也同樣存在這一問題。
    ????在用戶點擊后退按鈕后,瀏覽器到服務器再從服務器到瀏覽器這樣通常意思上的HTTP回路并沒有建立,僅僅只是用戶,瀏覽器和緩存進行了交互。所以,即使包含了清單3上的代碼來保護JSP頁面,當點擊后退按鈕時,這些代碼是不會執行的。
    ????緩存的好壞,真是仁者見仁智者見智。緩存的確提供了一些便利,但通常只在使用靜態的HTML頁面或基于圖形或影響的頁面你才能感受到。而另一方面,Web應用通常是基于數據的,數據通常是頻繁更改的。與從緩存中讀取并顯示過期的數據相比,提供最新的數據才是更重要的!
    ????幸運的是,HTTP頭信息“Expires”和“Cache-Control”為應用程序服務器提供了一個控制瀏覽器和代理服務器上緩存的機制。HTTP頭信息Expires告訴代理服務器它的緩存頁面何時將過期。HTTP1.1規范中新定義的頭信息Cache-Control可以通知瀏覽器不緩存任何頁面。當點擊后退按鈕時,瀏覽器重新訪問服務器已獲取頁面。如下是使用Cache-Control的基本方法:
    ????&#8226;??no-cache:強制緩存從服務器上獲取新的頁面
    ????&#8226;??no-store: 在任何環境下緩存不保存任何頁面
    ????HTTP1.0規范中的Pragma:no-cache等同于HTTP1.1規范中的Cache-Control:no-cache,同樣可以包含在頭信息中。
    ????通過使用HTTP頭信息的cache控制,第二個示例應用logoutSampleJSP2解決了logoutSampleJSP1的問題。logoutSampleJSP2與logoutSampleJSP1不同表現在如下代碼段中,這一代碼段加入進所有受保護的頁面中:


    //...
    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);
    }
    //...



    ??????通過設置頭信息和檢查HttpSession中的用戶名確保了瀏覽器不緩存頁面,同時,如果用戶未登陸,受保護的JSP頁面將不會發送到瀏覽器,取而代之的將是登陸頁面login.jsp。

    運行logoutSampleJSP2
    ??????運行logoutSampleJSP2后將回看到如下結果:
    ????&#8226;??當用戶退出后試圖點擊后退按鈕,瀏覽器并不會顯示受保護的頁面,它只會現實登陸頁login.jsp同時給出提示信息Session has ended. Please log in.
    ??????&#8226;??然而,當按了后退按鈕返回的頁是處理用戶提交數據的頁面時,IE和Avant瀏覽器將彈出如下信息提示:
    ?????????????? 警告:頁面已過期……(你肯定見過)
    ????選擇刷新后前一個JSP頁面將重新顯示在瀏覽器中。很顯然,這不是我們所想看到的因為它違背了logout動作的目的。發生這一現象時,很可能是一個惡意用戶在嘗試獲取其他用戶的數據。然而,這個問題僅僅出現在后退按鈕對應的是一個處理POST請求的頁面。

    記錄最后登陸時間
    ??????上述問題之所以出現是因為瀏覽器將其緩存中的數據重新提交了。這本文的例子中,數據包含了用戶名和密碼。無論是否給出安全警告信息,瀏覽器此時起到了負面作用。
    ????為了解決logoutSampleJSP2中出現的問題,logoutSampleJSP3的login.jsp在包含username和password的基礎上還包含了一個稱作lastLogon的隱藏表單域,此表單域動態的用一個long型值初始化。這個long型值是調用System.currentTimeMillis()獲取到的自1970年1月1日以來的毫秒數。當login.jsp中的form提交時,loginAction.jsp首先將隱藏域中的值與用戶數據庫中的值進行比較。只有當lastLogon表單域中的值大于數據庫中的值時Web應用才認為這是個有效的登陸。
    ????為了驗證登陸,數據庫中lastLogon字段必須以表單中的lastLogon值進行更新。上例中,當瀏覽器重復提交數據時,表單中的lastLogon值不比數據庫中的lastLogon值大,因此,loginAction轉到login.jsp頁面,并提示Session has ended.Please log in.清單5是loginAction中節選的代碼段:


    清單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 = &#39;" + userName + "&#39;");
    ?? }
    ?? 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);
    //...



    ??????為了實現上述方法,你必須記錄每個用戶的最后登陸時間。對于采用關系型數據庫安全域來說,這點可以可以通過在某個表中加上lastLogin字段輕松實現。LDAP以及其他的安全域需要稍微動下腦筋,但很顯然是可以實現的。
    ????表示最后登陸時間的方法有很多。示例logoutSampleJSP3利用了自1970年1月1日以來的毫秒數。這個方法在許多人在不同瀏覽器中用一個用戶賬號登陸時也是可行的。

    運行logoutSampleJSP3
    ??????運行示例logoutSampleJSP3將展示如何正確處理退出問題。一旦用戶退出,點擊瀏覽器上的后退按鈕在任何情況下都不會是受保護的頁面在瀏覽器上顯示出來。這個示例展示了如何正確處理退出問題而不需要額外的培訓。
    ????為了使代碼更簡練有效,一些冗余的代碼可以剔除掉。一種途徑就是把清單4中的代碼寫到一個單獨的JSP頁中,通過標簽<jsp:include>其他頁面也可以引用。

    Struts框架下的退出實現
    ??????與直接使用JSP或JSP/servlets相比,另一個可選的方案是使用Struts。為一個基于Struts的Web應用添加一個處理退出問題的框架可以優雅地不費氣力的實現。這部分歸功于Struts是采用MVC設計模式的因此將模型和視圖清晰的分開。另外,Java是一個面向對象的語言,其支持繼承,可以比JSP中的腳本更為容易地實現代碼重用。在Struts中,清單4中的代碼可以從JSP頁面中移植到Action類的execute()方法中。
    ????此外,我們還可以定義一個繼承Struts Action類的基本類,其execute()方法中包含了清單4中的代碼。通過使用類繼承機制,其他類可以繼承基本類中的通用邏輯來設置HTTP頭信息以及檢索HttpSession對象中的username字符串。這個基本類是一個抽象類并定義了一個抽象方法executeAction()。所有繼承自基類的子類都應實現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對象將找到名為sessionEnded的forward元素并跳轉到對應的path。如果找到了,子類將執行其實現了executeAction()方法的業務邏輯。因此,在配置文件struts-web.xml中為所有子類聲明個一名為sessionEnded的forward元素是必須的。清單7以secure1 action闡明了這樣一個聲明:

    清單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實現了executeAction()方法而不是覆蓋它。Secure1Action類不執行任何退出代碼,如清單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"));
    ?? }
    }



    ????只需要定義一個基類而不需要額外的代碼工作,上述解決方案是優雅而有效的。不管怎樣,將通用的行為方法寫成一個繼承StrutsAction的基類是許多Struts項目的共同經驗,值得推薦。

    局限性
    ????上述解決方案對JSP或基于Struts的Web應用都是非常簡單而實用的,但它還是有某些局限。在我看來,這些局限并不是至關緊要的。(局限性未翻譯,請參見原文)

    結論
    ????本文闡述了解決退出問題的方案,盡管方案簡單的令人驚訝,但卻在所有情況下都能有效地工作。無論是對JSP還是Struts,所要做的不過是寫一段不超過50行的代碼以及一個記錄用戶最后登陸時間的方法。在Web應用中混合使用這些方案能夠使擁護的私人數據不致泄露,同時,也能增加用戶的經驗。

    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.
    posted on 2006-12-20 20:33 蘆葦 閱讀(285) 評論(0)  編輯  收藏 所屬分類: 其他Struts
    主站蜘蛛池模板: 国产成人免费全部网站| 2021国内精品久久久久精免费| 最近2019中文字幕免费大全5 | av永久免费网站在线观看 | 欧美a级在线现免费观看| 久久精品亚洲一区二区三区浴池 | 中文字幕一区二区免费| 丁香亚洲综合五月天婷婷| 亚洲国产精品无码观看久久| 69成人免费视频| 精品亚洲国产成人| 免费H网站在线观看的| 亚洲喷奶水中文字幕电影| 1000部羞羞禁止免费观看视频| 久久精品国产亚洲| 亚洲综合无码一区二区痴汉| 国产高清不卡免费在线| 亚洲国产日韩在线| 国产精品四虎在线观看免费| 精品国产亚洲第一区二区三区| 日韩精品免费电影| 2022国内精品免费福利视频 | 免费成人福利视频| 亚洲电影在线播放| 国产精品jizz在线观看免费| 国产精品久久久久久亚洲小说| 亚洲综合亚洲综合网成人| 一个人免费视频在线观看www| 亚洲av鲁丝一区二区三区| 国产精品免费观看| 羞羞网站免费观看| 亚洲电影免费在线观看| 我要看WWW免费看插插视频| 老司机午夜免费视频| 亚洲国产精品久久久久网站| 在线a级毛片免费视频| 99re8这里有精品热视频免费| 777亚洲精品乱码久久久久久| 国产免费av片在线播放| 无码国产精品一区二区免费式芒果 | 国产午夜无码片免费|