Session Management in Java - HttpServlet, Cookies, URL Rewriting
Session Management in Java Servlet Web Applications is a very interesting topic. Session in Java Servlet are managed through different ways, such as Cookies, HttpSession API, URL rewriting etc. This is the third article in the series of Web Applications tutorial in Java, you might want to check out earlier two articles too.
Session Management in Java
This article is aimed to explain about session management in servlets using different techniques and with example programs.
-
What is a Session?
HTTP protocol and Web Servers are stateless, what it means is that for web server every request is a new request to process and they can’t identify if it’s coming from client that has been sending request previously. But sometimes in web applications, we should know who the client is and process the request accordingly. For example, a shopping cart application should know who is sending the request to add an item and in which cart the item has to be added or who is sending checkout request so that it can charge the amount to correct client. Session is a conversional state between client and server and it can consists of multiple request and response between client and server. Since HTTP and Web Server both are stateless, the only way to maintain a session is when some unique information about the session (session id) is passed between server and client in every request and response. There are several ways through which we can provide unique identifier in request and response.
-
User Authentication - This is the very common way where we user can provide authentication credentials from the login page and then we can pass the authentication information between server and client to maintain the session. This is not very effective method because it wont work if the same user is logged in from different browsers.
-
HTML Hidden Field - We can create a unique hidden field in the HTML and when user starts navigating, we can set its value unique to the user and keep track of the session. This method can’t be used with links because it needs the form to be submitted every time request is made from client to server with the hidden field. Also it’s not secure because we can get the hidden field value from the HTML source and use it to hack the session.
-
URL Rewriting - We can append a session identifier parameter with every request and response to keep track of the session. This is very tedious because we need to keep track of this parameter in every response and make sure it’s not clashing with other parameters.
-
Cookies - Cookies 是 Web 服务器在响应标头中发送的一小段信息,存储在浏览器的 cookies 中。当客户端发出进一步的请求时,它会将 cookie 添加到请求标头中,我们可以利用它来跟踪会话。我们可以使用 cookies 来维护会话,但如果客户端禁用 cookies,那么它将无法工作。
-
会话管理 API - 会话管理 API 建立在上述会话跟踪方法之上。上述所有方法的一些主要缺点是:
- 大多数时候我们不想只跟踪会话,我们必须将一些数据存储到会话中,以便在将来的请求中使用。如果我们尝试实现这一点,这将需要付出很多努力。
- 上述所有方法本身都不完整,并非所有方法在特定情况下都有效。因此,我们需要一种解决方案,能够在所有情况下利用这些会话跟踪方法来提供会话管理。
这就是为什么我们需要会话管理 API,并且 J2EE Servlet 技术附带了我们可以使用的会话管理 API。
-
-
Java 中的会话管理 - Cookies
在 Web 应用程序中,Cookie 被广泛使用,用于根据您的选择个性化响应或跟踪会话。在继续介绍 Servlet 会话管理 API 之前,我想展示如何通过一个小型 Web 应用程序使用 Cookie 跟踪会话。我们将创建一个动态 Web 应用程序ServletCookieExample,其项目结构如下图所示。Web应用程序的部署描述符 web.xml 为:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns="https://java.sun.com/xml/ns/javaee" xsi:schemaLocation="https://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <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); } } }
请注意,我们为响应设置了 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" "https://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; 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 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,但我们可以将最大期限设置为 0,这样它就会立即从客户端浏览器中删除。当我们运行上述应用程序时,我们会得到如下图所示的响应。
-
Java Servlet 中的会话 - HttpSession
Servlet API 通过
HttpSession
接口提供会话管理。我们可以使用以下方法从 HttpServletRequest 对象获取会话。HttpSession 允许我们将对象设置为可以在将来的请求中检索的属性。- HttpSession getSession() - 此方法始终返回一个 HttpSession 对象。它返回与请求关联的会话对象,如果请求未关联会话,则创建一个新会话并返回它。
- HttpSession getSession(boolean flag) ——如果请求有会话,则此方法返回 HttpSession 对象,否则返回 null。
HttpSession的一些重要方法包括:
- String getId() - 返回包含分配给此会话的唯一标识符的字符串。
- Object getAttribute(String name) - 返回此会话中与指定名称绑定的对象,如果未在该名称下绑定任何对象,则返回 null。其他一些使用会话属性的方法包括
getAttributeNames()
、removeAttribute(String name)
和setAttribute(String name, Object value)
。 - long getCreationTime() - 返回此会话的创建时间,以 GMT 1970 年 1 月 1 日午夜以来的毫秒数计算。我们可以使用
getLastAccessedTime()
方法获取上次访问时间。 - setMaxInactiveInterval(int interval) - 指定客户端请求之间的时间间隔(以秒为单位),超过此时间间隔,servlet 容器将使此会话失效。我们可以从
getMaxInactiveInterval()
方法中获取会话超时值。 - ServletContext getServletContext() - 返回应用程序的 ServletContext 对象。
- boolean isNew() - 如果客户端尚不知道该会话或者客户端选择不加入该会话,则返回 true。
- void invalidate() - 使此会话无效然后解除与其绑定的所有对象。
了解 JSESSIONID Cookie
当我们使用 HttpServletRequest getSession() 方法并创建一个新请求时,它会创建新的 HttpSession 对象,并向响应对象添加一个名为 JSESSIONID 、值为会话 id 的 Cookie。此 cookie 用于在来自客户端的进一步请求中标识 HttpSession 对象。如果在客户端禁用了 cookie 并且我们正在使用 URL 重写,那么此方法将使用请求 URL 中的 jsessionid 值来查找相应的会话。JSESSIONID cookie 用于会话跟踪,因此我们不应将其用于我们的应用程序,以避免任何与会话相关的问题。让我们看一个使用 HttpSession 对象的会话管理示例。我们将在 Eclipse 中创建一个动态 Web 项目,其中 servlet 上下文为 ServletHttpSessionExample。项目结构如下图所示。login.html与前面的示例相同,并在 web.xml 中定义为应用程序的欢迎页面 LoginServlet servlet 将创建会话并设置我们可以在其他资源或未来请求中使用的属性。
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" "https://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 资源时,容器会自动为其创建会话,因此我们无法检查会话是否为空,以确保用户是否已通过登录页面,因此我们使用会话属性来验证请求。CheckoutPage.jsp 是另一个页面,其代码如下所示。
<%@ page language="java" contentType="text/html; charset=US-ASCII" pageEncoding="US-ASCII"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "https://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 中打印与会话 ID 相同的值。下面的图片显示了我们的 Web 应用程序的执行情况。
-
Java Servlet 中的会话管理 - URL 重写
正如我们在上一节中看到的,我们可以使用 HttpSession 管理会话,但如果我们在浏览器中禁用 cookie,它将无法工作,因为服务器将不会从客户端接收 JSESSIONID cookie。Servlet API 提供了对 URL 重写的支持,在这种情况下我们可以使用它来管理会话。最好的部分是,从编码的角度来看,它非常易于使用,并且涉及一个步骤 - 编码 URL。Servlet URL 编码的另一个好处是它是一种后备方法,只有在浏览器 cookie 被禁用时才会启动。我们可以使用 HttpServletResponse
encodeURL()
方法对 URL 进行编码,如果我们必须将请求重定向到另一个资源并且我们想要提供会话信息,我们可以使用encodeRedirectURL()
方法。我们将创建一个与上述类似的项目,不同之处在于我们将使用 URL 重写方法来确保即使在浏览器中禁用 cookie 的情况下会话管理也能正常工作。Eclipse 中的 ServletSessionURLRewriting 项目结构如下图所示。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" "https://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" "https://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 页面上,用户名为空,因为浏览器未发送上次响应中发送的 cookie。如果没有禁用 cookie,您将不会在 URL 中看到 jsessionid,因为在这种情况下 Servlet Session API 将使用 cookie。
这就是 Java Servlet 中的会话管理的全部内容,我们将在后续文章中探讨 Servlet 过滤器、监听器和 Cookies。更新:请查看本系列的下一篇文章Servlet 过滤器。
下载项目
-
下载 Servlet Cookie 示例项目
-
下载 Servlet HttpSession 示例项目
-
下载 Servlet 会话 URL 重写示例项目