如何處理退出後,後退按鈕依舊顯示緩存的問題

在一個有密碼保護的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將會出現以下幾種情形:

  1) 若是用戶沒有登錄,Web應用將會正確停止受保護頁面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的執行,也就是說,假如用戶在瀏覽器地址欄中直接敲入受保護JSP頁的地址試圖訪問,Web應用將自動跳轉到登錄頁並提示Session has ended.Please log in.

  2) 一樣的,當一個用戶已經退出,Web應用也會正確停止受保護頁面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的執行

  3) 用戶退出後,若是點擊瀏覽器上的後退按鈕,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的基本方法:

  1) no-cache:強制緩存從服務器上獲取新的頁面

  2) 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後將回看到以下結果:

  1) 當用戶退出後試圖點擊後退按鈕,瀏覽器並不會顯示受保護的頁面,它只會現實登錄頁login.jsp同時給出提示信息Session has ended. Please log in.

  2) 然而,當按了後退按鈕返回的頁是處理用戶提交數據的頁面時,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,所要作的不過是寫一段不超過50行的代碼以及一個記錄用戶最後登錄時間的方法。在Web應用中混合使用這些方案可以使擁護的私人數據不致泄露,同時,也能增長用戶的經驗。
javascript

轉載自http://hi.baidu.com/beer_zh/blog/item/6991f838503d29e8b311c703.htmlhtml

相關文章
相關標籤/搜索