Java中的會話管理——HttpServlet,Cookies,URL Rewriting(譯)
參考谷歌翻譯,關鍵字直接使用英文,原文地址:http://www.journaldev.com/1907/java-session-management-servlet-httpsession-url-rewriting
Java Web應用程序中的會話管理(Session Management)是一個非常有趣的話題。Java Servlet中的會話通過不同的方式進行管理,例如Cookie,HttpSession API,URL重寫等。
這是Java Web應用程序系列教程中的第三篇文章,您可能還需要先閱讀兩篇文章。
Java中的會話管理
本文旨在使用不同的技術和示例程序解釋servlet中的會話管理。
- 什麽是Session?
- Java中的會話管理——Cookie
- Java Servlet中的Session——HttpSession
- Java Servlet中的會話管理——URL重寫
1、什麽是Session?
HTTP協議和Web服務器都是無狀態的,這意味著對Web服務器而言,每個請求都是一個新的進程,它無法確定請求是否來自先前發送過請求的客戶端。
但有時候,我們需要知道客戶端是誰,並且相應地處理請求。例如,購物車應用程序應該知道誰正在發送添加商品的請求,誰正在發送結帳的請求,以便可以無誤地進行收費或者把商品添加到正確的購物車中。
我們可以通過以下幾種方式在請求和響應中提供唯一的標識符。
- 用戶認證——這是一種很常用的方法,用戶從登錄頁面提供認證憑證,然後我們可以在服務器和客戶端之間傳遞認證信息來維護會話。但是,這並非是一個非常有效的方法,因為一旦相同的用戶從不同的瀏覽器登錄,它將無法正常地工作。
- HTML隱藏字段(HTML Hidden Field)——我們可以在HTML中創建一個唯一的隱藏字段,當用戶開始導航時,把它設置為對用戶的唯一值,並跟蹤會話。這種方法不能與超鏈接一起使用,因為它要求每次從客戶端提交的表單都具備隱藏字段。此外,它不夠安全,因為我們可以從HTML源代碼中獲得隱藏的字段值,並利用它來破壞會話。
- URL重寫(URL Rewriting)——我們可以給每個請求和響應附加會話標識符參數,以跟蹤會話。這是非常繁瑣的,因為我們需要在每個響應中跟蹤這個參數,並確保它不會與其他參數沖突。
- Cookie——Cookie是由Web服務器發送,並存儲在瀏覽器中的小塊信息。當進一步請求時,瀏覽器將Cookie添加到Request Header中,我們可以利用它來跟蹤會話。但如果客戶端禁用cookies,那麽它將不起作用。
- Session Management API——會話管理API是基於上述方法構建的,用於會話跟蹤。上述方法的一些主要缺點是:1)、大多數時候,我們不僅要跟蹤會話,還需要將一些數據存儲到會話中,以便在將來的請求中使用。如果我們試圖實現這一點,需要付出很多努力。2)、所有上述方法本身都不完整,所有這些方法都會在特定情況下不起作用。因此,我們需要一種解決方案,可以利用這個方法在所有情況下提供會話管理。
這就是為什麽我們需要Session Management API,以及為什麽J2EE技術附帶了Session Management API供我們使用。
2、Java中的會話管理——Cookie
在Web應用程序中Cookie應用得很多,它可以根據你的選擇來個性化響應或跟蹤會話。在轉到Session Management API之前,我想通過一個小型Web應用程序來展示如何利用Cookie跟蹤會話。
我們將創建一個具有如下圖所示項目結構的動態Web應用程序ServletCookieExample。
(原文ide使用的是Eclipse,使用Intellij Idea的朋友可以參考Intellij Idea 創建Web項目入門 by 我是一名老菜鳥)
web應用程序的部署描述文件web.xml:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <display-name>ServletCookieExample</display-name> <welcome-file-list> <welcome-file>login.html</welcome-file> </welcome-file-list> </web-app>
我們的應用程序的歡迎頁面是login.html,我們將從用戶那裏獲取身份驗證的詳細信息。
<!DOCTYPE html> <html> <head> <meta charset="US-ASCII"> <title>Login Page</title> </head> <body> <form action="LoginServlet" method="post"> Username: <input type="text" name="user"> <br> Password: <input type="password" name="pwd"> <br> <input type="submit" value="Login"> </form> </body> </html>
這是一個負責處理“登錄請求”的LoginServlet。
package com.journaldev.servlet.session; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class LoginServlet */ @WebServlet("/LoginServlet") public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; private final String userID = "Pankaj"; private final String password = "journaldev"; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // get request parameters for userID and password String user = request.getParameter("user"); String pwd = request.getParameter("pwd"); if(userID.equals(user) && password.equals(pwd)){ Cookie loginCookie = new Cookie("user",user); //setting cookie to expiry in 30 mins loginCookie.setMaxAge(30*60); response.addCookie(loginCookie); response.sendRedirect("LoginSuccess.jsp"); }else{ RequestDispatcher rd = getServletContext().getRequestDispatcher("/login.html"); PrintWriter out= response.getWriter(); out.println("<font color=red>Either user name or password is wrong.</font>"); rd.include(request, response); } } }
請註意我們設置在response中的cookie,它在之後將被轉發到LoginSuccess.jsp,此cookie將用於跟蹤會話。還要註意,“cookie超時”設置為30分鐘。理想情況下,應該有一個復雜的邏輯來設置跟蹤會話的cookie值,以便它不會與別的請求相沖突。
<%@ page language="java" contentType="text/html; charset=US-ASCII" pageEncoding="US-ASCII"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=US-ASCII"> <title>Login Success Page</title> </head> <body> <% String userName = null; Cookie[] cookies = request.getCookies(); if(cookies !=null){ for(Cookie cookie : cookies){ if(cookie.getName().equals("user")) userName = cookie.getValue(); } } if(userName == null) response.sendRedirect("login.html"); %> <h3>Hi <%=userName %>, Login successful.</h3> <br> <form action="LogoutServlet" method="post"> <input type="submit" value="Logout" > </form> </body> </html>
請註意,如果嘗試直接訪問JSP,它將自動把我們轉到“登錄頁面”。當我們點擊註銷按鈕時,應該確保cookie已經從瀏覽器中刪除。
package com.journaldev.servlet.session; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class LogoutServlet */ @WebServlet("/LogoutServlet") public class LogoutServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); Cookie loginCookie = null; Cookie[] cookies = request.getCookies(); if(cookies != null){ for(Cookie cookie : cookies){ if(cookie.getName().equals("user")){ loginCookie = cookie; break; } } } if(loginCookie != null){ loginCookie.setMaxAge(0); response.addCookie(loginCookie); } response.sendRedirect("login.html"); } }
沒有方法可以刪除cookie,但是我們可以將cookie的maximum age設置為0,它將立刻被瀏覽器移除。
當我們運行上面的應用程序時,將得到如下響應。
3、Java Servlet中的Session——HttpSession
Servlet API通過Http Session接口提供會話管理。我們可以使用以下方法從HttpServletRequest對象中獲取session。HttpSession允許我們將對象設置為屬性,以便在將來的請求中能夠檢索。
- HttpSession getSession() – 這個方法總是返回一個HttpSession對象,如果HttpServletRequest對象沒有與session關聯,則創建一個新的session返回。
- HttpSession getSession(boolean flag) – 返回一個HttpSession對象,如果HttpServletRequest對象沒有與session關聯就返回null。
HttpSession的一些重要方法是:
- String getId() – 返回一個字符串,這個字符串包含了該session的唯一標識符。
- Object getAttribute(String name) – 返回在session中用指定名稱綁定的對象,如果沒有對象綁定在該名稱下,則返回null。其它與會話屬性相關的方法是
getAttributeNames()
,removeAttribute(String name)
和setAttribute(String name, Object value)
. - long getCreationTime() – 返回此session創建的時間,起始時間是1970年1月1日GMT格林尼治時間,單位為毫秒。我們可以用
getLastAccessedTime()
方法獲得最後訪問的時間。 - setMaxInactiveInterval(int interval) – 用來指定“session超時值”,以秒為單位,servlet容器將會在這段時間內保持session有效,我們可以用
getMaxInactiveInterval()
方法獲取“session超時值”。 - ServletContext getServletContext() – 返回Web應用的ServletContext對象。
- boolean isNew() – 如果客戶端還不知道會話或者客戶端選擇不加入會話,則返回true。
- void invalidate() – 使這個對話無效,並且解除所有綁定的對象。
理解JSESSIONID Cookie
當使用HttpServletRequest的getSession()方法時,我們將獲得一個新的HttpSession對象,與此同時,一個特殊的Cookie將被添加到對應的Response對象,這個Cookie名字叫做JSESSIONID,它的值就是“session id”。這個cookie用於在客戶端的進一步請求中識別HttpSession對象。如果Cookie在客戶端被禁用,並且我們正在使用URL重寫,則該方法使用request URL中的JSESSIONID值來查找相應的會話。JSESSIONID cookie用於會話跟蹤,因此我們不應該將其用於我們的應用程序,以避免造成任何會話相關的問題。
讓我們看看使用HttpSession對象的會話管理示例。我們將在Eclipse中創建一個動態Web項目,項目結構如下圖所示。
login.html與前面的示例一樣,並定義應用程序的歡迎頁面。
LoginServlet servlet將創建session,並在其中設置我們可以在其他資源或將來請求中使用的屬性。
package com.journaldev.servlet.session; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * Servlet implementation class LoginServlet */ @WebServlet("/LoginServlet") public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; private final String userID = "admin"; private final String password = "password"; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // get request parameters for userID and password String user = request.getParameter("user"); String pwd = request.getParameter("pwd"); if(userID.equals(user) && password.equals(pwd)){ HttpSession session = request.getSession(); session.setAttribute("user", "Pankaj"); //setting session to expiry in 30 mins session.setMaxInactiveInterval(30*60); Cookie userName = new Cookie("user", user); userName.setMaxAge(30*60); response.addCookie(userName); response.sendRedirect("LoginSuccess.jsp"); }else{ RequestDispatcher rd = getServletContext().getRequestDispatcher("/login.html"); PrintWriter out= response.getWriter(); out.println("<font color=red>Either user name or password is wrong.</font>"); rd.include(request, response); } } }
我們的LoginSuccess.jsp 如下所示。
<%@ page language="java" contentType="text/html; charset=US-ASCII" pageEncoding="US-ASCII"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=US-ASCII"> <title>Login Success Page</title> </head> <body> <% //allow access only if session exists String user = null; if(session.getAttribute("user") == null){ response.sendRedirect("login.html"); }else user = (String) session.getAttribute("user"); String userName = null; String sessionID = null; Cookie[] cookies = request.getCookies(); if(cookies !=null){ for(Cookie cookie : cookies){ if(cookie.getName().equals("user")) userName = cookie.getValue(); if(cookie.getName().equals("JSESSIONID")) sessionID = cookie.getValue(); } } %> <h3>Hi <%=userName %>, Login successful. Your Session ID=<%=sessionID %></h3> <br> User=<%=user %> <br> <a href="CheckoutPage.jsp">Checkout Page</a> <form action="LogoutServlet" method="post"> <input type="submit" value="Logout" > </form> </body> </html>
當使用JSP資源時,容器會自動為其創建一個session,所以我們不能檢查會話是否為空,來確保用戶是否通過登錄頁面,因此我們使用session的屬性來驗證請求。
CheckoutPage.jsp是另外一個頁面,它的代碼如下所示。
<%@ page language="java" contentType="text/html; charset=US-ASCII" pageEncoding="US-ASCII"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=US-ASCII"> <title>Login Success Page</title> </head> <body> <% //allow access only if session exists if(session.getAttribute("user") == null){ response.sendRedirect("login.html"); } String userName = null; String sessionID = null; Cookie[] cookies = request.getCookies(); if(cookies !=null){ for(Cookie cookie : cookies){ if(cookie.getName().equals("user")) userName = cookie.getValue(); } } %> <h3>Hi <%=userName %>, do the checkout.</h3> <br> <form action="LogoutServlet" method="post"> <input type="submit" value="Logout" > </form> </body> </html>
我們的LogoutServlet代碼如下所示。
package com.journaldev.servlet.session; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * Servlet implementation class LogoutServlet */ @WebServlet("/LogoutServlet") public class LogoutServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); Cookie[] cookies = request.getCookies(); if(cookies != null){ for(Cookie cookie : cookies){ if(cookie.getName().equals("JSESSIONID")){ System.out.println("JSESSIONID="+cookie.getValue()); break; } } } //invalidate the session if exists HttpSession session = request.getSession(false); System.out.println("User="+session.getAttribute("user")); if(session != null){ session.invalidate(); } response.sendRedirect("login.html"); } }
請註意,我在日誌中打印了JSESSIONID cookie的值,你可以檢查服務器日誌在哪裏打印了與LoginSuccess.jsp中的Session id相同的值。
下圖顯示了我們的Web應用程序的執行。
4、Java Servlet中的會話管理 - URL重寫
正如我們在上一節中看到的,我們可以使用HttpSession來管理一個會話,但是如果我們在瀏覽器中禁用了cookies,它將不起作用,因為服務器將不會從客戶端收到JSESSIONID cookie。Servlet API提供了對URL重寫的支持,以便我們可以在這種情況下管理會話。
從編碼的角度來看,它很容易使用,它涉及到了一個步驟 - 編碼URL(encoding the URL)。編碼URL的另一個好處是它是一種“後備方法”,只有在瀏覽器Cookie被禁用的情況下才會啟用。
我們可以使用HttpServletResponse的 encodeURL()
方法對URL進行編碼,如果我們必須將請求重定向到另一個資源,並且提供會話信息,那麽我們可以使用encodeRedirectURL()
方法。
我們將創建一個類似的項目,這個項目將使用URL重寫來確保會話管理工作正常(即使瀏覽器禁用了cookies)。
package com.journaldev.servlet.session; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * Servlet implementation class LoginServlet */ @WebServlet("/LoginServlet") public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; private final String userID = "admin"; private final String password = "password"; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // get request parameters for userID and password String user = request.getParameter("user"); String pwd = request.getParameter("pwd"); if(userID.equals(user) && password.equals(pwd)){ HttpSession session = request.getSession(); session.setAttribute("user", "Pankaj"); //setting session to expiry in 30 mins session.setMaxInactiveInterval(30*60); Cookie userName = new Cookie("user", user); response.addCookie(userName); //Get the encoded URL string String encodedURL = response.encodeRedirectURL("LoginSuccess.jsp"); response.sendRedirect(encodedURL); }else{ RequestDispatcher rd = getServletContext().getRequestDispatcher("/login.html"); PrintWriter out= response.getWriter(); out.println("<font color=red>Either user name or password is wrong.</font>"); rd.include(request, response); } } }
<%@ page language="java" contentType="text/html; charset=US-ASCII" pageEncoding="US-ASCII"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=US-ASCII"> <title>Login Success Page</title> </head> <body> <% //allow access only if session exists String user = null; if(session.getAttribute("user") == null){ response.sendRedirect("login.html"); }else user = (String) session.getAttribute("user"); String userName = null; String sessionID = null; Cookie[] cookies = request.getCookies(); if(cookies !=null){ for(Cookie cookie : cookies){ if(cookie.getName().equals("user")) userName = cookie.getValue(); if(cookie.getName().equals("JSESSIONID")) sessionID = cookie.getValue(); } }else{ sessionID = session.getId(); } %> <h3>Hi <%=userName %>, Login successful. Your Session ID=<%=sessionID %></h3> <br> User=<%=user %> <br> <!-- need to encode all the URLs where we want session information to be passed --> <a href="<%=response.encodeURL("CheckoutPage.jsp") %>">Checkout Page</a> <form action="<%=response.encodeURL("LogoutServlet") %>" method="post"> <input type="submit" value="Logout" > </form> </body> </html>
<%@ page language="java" contentType="text/html; charset=US-ASCII" pageEncoding="US-ASCII"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=US-ASCII"> <title>Login Success Page</title> </head> <body> <% String userName = null; //allow access only if session exists if(session.getAttribute("user") == null){ response.sendRedirect("login.html"); }else userName = (String) session.getAttribute("user"); String sessionID = null; Cookie[] cookies = request.getCookies(); if(cookies !=null){ for(Cookie cookie : cookies){ if(cookie.getName().equals("user")) userName = cookie.getValue(); } } %> <h3>Hi <%=userName %>, do the checkout.</h3> <br> <form action="<%=response.encodeURL("LogoutServlet") %>" method="post"> <input type="submit" value="Logout" > </form> </body> </html>
package com.journaldev.servlet.session; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * Servlet implementation class LogoutServlet */ @WebServlet("/LogoutServlet") public class LogoutServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); Cookie[] cookies = request.getCookies(); if(cookies != null){ for(Cookie cookie : cookies){ if(cookie.getName().equals("JSESSIONID")){ System.out.println("JSESSIONID="+cookie.getValue()); } cookie.setMaxAge(0); response.addCookie(cookie); } } //invalidate the session if exists HttpSession session = request.getSession(false); System.out.println("User="+session.getAttribute("user")); if(session != null){ session.invalidate(); } //no encoding because we have invalidated the session response.sendRedirect("login.html"); } }
當我們在禁用Cookie的狀況下運行此項目時,將顯示如下響應頁面,請註意瀏覽器地址欄URL中的jsessionid。另外請註意,在LoginSuccess頁面上,用戶名是null,因為瀏覽器沒有發送最後一個響應所發送的cookie。
如果Cookie未被禁用,你將不會在URL中看到jsessionid,因為Servlet Session API會在這種情況下使用cookie。
原文地址:http://www.journaldev.com/1907/java-session-management-servlet-httpsession-url-rewriting
Java中的會話管理——HttpServlet,Cookies,URL Rewriting(譯)