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