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 中的会话管理。

  1. 什么是 Session?
  2. Java 中的会话管理—— Cookie
  3. Java Servlet 中的Session—— HttpSession
  4. Java Servlet 中的会话管理—— URL 重写

1、什么是 Session?

HTTP 协议和 Web 服务器都是无状态的,这意味着对 Web 服务器而言,每个请求都是一个新的进程,它无法确定请求是否来自先前发送过请求的客户端。
但有时候,我们需要知道客户端是谁,并且相应地处理请求。例如,购物车应用程序应该知道谁正在发送添加商品的请求,谁正在发送结帐的请求,以便可以无误地进行收费或者把商品添加到正确的购物车中。
Session是客户端和服务器之间的会话状态,它可以由客户端和服务器之间的多个请求和响应组成。由于 HTTP 和 Web 服务器都是无状态的,维护会话的唯一方法是在每个请求和响应中传递关于会话的唯一信息(session id)
我们可以通过以下几种方式在请求和响应中提供唯一的标识符。

  1. 用户认证——这是一种很常用的方法,用户从登录页面提供认证凭证,然后我们可以在服务器和客户端之间传递认证信息来维护会话。但是,这并非是一个非常有效的方法,因为一旦相同的用户从不同的浏览器登录,它将无法正常地工作。
  2. HTML隐藏字段(HTML Hidden Field)——我们可以在HTML中创建一个唯一的隐藏字段,当用户开始导航时,把它设置为对用户的唯一值,并跟踪会话。这种方法不能与超链接一起使用,因为它要求每次从客户端提交的表单都具备隐藏字段。此外,它不够安全,因为我们可以从HTML源代码中获得隐藏的字段值,并利用它来破坏会话。
  3. URL 重写(URL Rewriting)——我们可以给每个请求和响应附加会话标识符参数,以跟踪会话。这是非常繁琐的,因为我们需要在每个响应中跟踪这个参数,并确保它不会与其他参数冲突。
  4. Cookie —— Cookie 是由Web服务器发送,并存储在浏览器中的小块信息。当进一步请求时,浏览器将 Cookie 添加到 Request Header 中,我们可以利用它来跟踪会话。但如果客户端禁用 cookies,那么它将不起作用。
  5. 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>

【注释】<display-name> 元素是可选的,详细信息参考 oracle 文档;<welcome-file-list>元素同样是可选的,它是一个包含<welcome-file>元素的有序列表,可以结合servlet-mapping使用,有兴趣可以参考这里HowToUseAServletAsYourMainWebPage

应用程序的欢迎页面是 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>

【注释】HTML tag 的百科全书,关于 input 元素的详细信息可以参考——><input>

这是一个负责处理“登录请求”的 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允许我们将对象设置为属性,以便在将来的请求中能够检索。

  1. HttpSession getSession() – 这个方法总是返回一个HttpSession对象,如果HttpServletRequest对象没有与session关联,则创建一个新的session返回。
  2. HttpSession getSession(boolean flag) – 返回一个HttpSession对象,如果HttpServletRequest对象没有与session关联就返回null

HttpSession的一些重要方法是:

  1. String getId() – 返回一个字符串,这个字符串包含了该session的唯一标识符。
  2. Object getAttribute(String name) –  返回在session中用指定名称绑定的对象,如果没有对象绑定在该名称下,则返回null。其它与会话属性相关的方法是 getAttributeNames()removeAttribute(String name) 和 setAttribute(String name, Object value).
  3. long getCreationTime() – 返回此session创建的时间,起始时间是1970年1月1日GMT格林尼治时间,单位为毫秒。我们可以用getLastAccessedTime()方法获得最后访问的时间。
  4. setMaxInactiveInterval(int interval) – 用来指定“session超时值”,以秒为单位,servlet容器将会在这段时间内保持session有效,我们可以用getMaxInactiveInterval()方法获取“session超时值”。
  5. ServletContext getServletContext() – 返回Web应用的ServletContext对象。
  6. boolean isNew() – 如果客户端还不知道会话或者客户端选择不加入会话,则返回true。
  7. 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>

【注释】我们可以观察到,不论是在ServletLogin.java还是LoginSuccess.jsp中都没有手动创建JSESSIONED相关的cookie,然而,在我们第一次登陆的时候,可以在响应首部中看到Set-Cookie: JSESSIONID=2E2F69D80DF292A553719ACF32A901FF; 

当使用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>

【注释】在点击CheckoutPage切换页面的时候,cookies将继续传递(包含user、JSESSIONID),而session也一直存在。

我们的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");
    }

}

【注释】退出前使session无效化,虽然这个时候JSESSIONID还保留在浏览器,但是当你重新登陆的时候,值就不再是原来那个值了(会被新的JSESSION值所覆盖)。

请注意,我在日志中打印了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()方法。

【注释】这段翻译得很乱!我在网上查了一些资料,对两者的差异仍没有搞清楚,encodeURL()和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

posted @ 2017-06-05 23:24  xkfx  阅读(1174)  评论(0编辑  收藏  举报