正確優雅地解決使用者退出——JSP及Struts解決方案

來源:互聯網
上載者:User
摘要

      在一個有密碼保護的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 = '" + 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);
//...

    本文所附樣本均以關係型資料庫作為安全域,但本文所闡述的觀點對任何類型的安全域都是適用的。

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將會出現如下幾種情形:
    ?  如果使用者沒有登陸,Web應用將會正確中止受保護頁面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的執行,也就是說,假如使用者在瀏覽器地址欄中直接敲入受保護JSP頁的地址試圖訪問,Web應用將自動跳轉到登陸頁並提示Session has ended.Please log in.
      ?  同樣的,當一個使用者已經退出,Web應用也會正確中止受保護頁面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的執行
    ?  使用者退出後,如果點擊瀏覽器上的後退按鈕,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”為應用程式伺服器提供了一個控制瀏覽器和Proxy 伺服器上緩衝的機制。HTTP頭資訊Expires告訴Proxy 伺服器它的快取頁面面何時將到期。HTTP1.1規範中新定義的頭資訊Cache-Control可以通知瀏覽器不緩衝任何頁面。當點擊後退按鈕時,瀏覽器重新訪問伺服器已擷取頁面。如下是使用Cache-Control的基本方法:
    ?  no-cache:強制緩衝從伺服器上擷取新的頁面
    ?  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後將回看到如下結果:
    ?  當使用者退出後試圖點擊後退按鈕,瀏覽器並不會顯示受保護的頁面,它只會現實登陸頁login.jsp同時給出提示資訊Session has ended. Please log in.
      ?  然而,當按了後退按鈕返回的頁是處理使用者提交資料的頁面時,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 = '" + 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);
//...

      為了實現上述方法,你必須記錄每個使用者的最後登陸時間。對於採用關係型資料庫安全域來說,這點可以可以通過在某個表中加上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應用中混合使用這些方案能夠使擁護的私人資料不致泄露,同時,也能增加使用者的經驗。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.