第5章?会话管理
  
  本章要点:
* 能理解会话的概念和原理。
* 能理解Cookie的生命周期,会熟练使用浏览器查看Cookie数据。
* 能理解Cookie的运行机制,会熟练编写Cookie相关代码。
* 能理解Session的生命周期,会熟练使用浏览器查看Session的Id数据。
* 能理解Session的运行机制,会熟练编写Session相关代码。
  
?5.1
  

会话概述
  
  在计算机术语中,会话是指一个终端用户与交互系统进行通信的过程,例如,在ATM机上,用户从输入账户密码进入操作系统到退出操作系统就是一个会话过程。
  在Java Web应用中,通常客户端浏览器通过“请求响应”模式访问同一个网站的不同页面,从开始访问这个网站直到结束的整个过程称为一次会话。一次会话过程,包含了多次客户端与服务器端之间的请求响应过程。如图5-1所示,图中的三次请求响应可称为一次会话。
  

图5-1  会话的概念
  从理论上来说,每次客户端与服务器端之间的请求响应是独立的,与之前后的请求响应无关。主要原因是请求响应是基于HTTP通信协议,而在HTTP通信协议规范下每个请求都是完全独立的,每个请求包含了处理这个请求所需的完整数据,发送请求不涉及状态变更。因此,通常认为HTTP通信协议是无状态的。
  在图5-1中,从基于HTTP通信协议的请求响应来分析。
  第一次用户登录请求,浏览器会在请求数据包中封装登录表单中的用户名、密码等信息发送给服务器端。
  第二次查看商品详情请求,浏览器会在请求数据包中封装用户单击的商品编号等信息发送给服务器端。
  第三次添加商品至购物车请求,浏览器会在请求数据包中封装用户购买的商品编号、数量、金额等信息发送给服务器端。
  因为每次的请求都是独立的,所以在第三次请求中,服务器端接收的请求数据中仅包含商品信息,不包含用户信息,无法把商品放入用户购物车。即服务器端不能为不同的客户端处理和响应不同的信息,不能支撑交互式动态网站的实现。
  若从会话角度来说,上述三个请求显然属于同一个会话。对服务器端来说,在同一个会话中交互的是同一个用户,如果能“识别”这个用户,那么就能解决HTTP通信协议无状态的问题,即可实现交互式动态网站。
  为了使服务器端具有“识别”不同客户端的能力,产生了Cookie和Session两种技术,它们都属于Java Web应用中的会话管理技术。
  
?5.2
  

Cookie技术
* 5.2.1  Cookie概述
  HTTP通信协议本身是无状态的,这与HTTP通信协议原本的设计目的是相符的。在早期的互联网中,客户端只需要简单地向服务器端发送请求,服务器端依据请求内容返回相应的响应内容。无论是客户端还是服务器端都没有必要记录彼此之间的交互行为,每次请求之间都是独立的,好比一个顾客和一个非会员制超市之间的关系一样。
  然而随着互联网的普及,出现了个性化的交互式动态网站服务的需求。这种需求一方面促使HTML逐步添加了表单、脚本、DOM等客户端行为;另一方面在服务器端则出现了CGI规范以响应客户端的动态请求,作为传输载体的HTTP通信协议也添加了文件上传、Cookie等特性。其中Cookie就是为了解决HTTP通信协议无状态的缺陷所做出的努力。
  Cookie是设计交互式动态网页的一项重要技术,它可以将一些简短的数据存储在客户端,这些数据变量称为Cookie。当客户端浏览器向服务器端发送请求时,浏览器会自动在请求中携带属于当前服务器端的Cookie数据,服务器端在接收请求时可以读取Cookie数据,从而获得了“识别”不同客户端的能力。


* 5.2.2  Cookie的运行机制
  超市为了吸引顾客往往会采用会员制,给会员提供折扣、积分、节日赠品等多项福利。通常顾客首次来超市时,超市会制作并发放会员卡给顾客。后续顾客来超市购物时带上会员卡,就可以享受超市提供的会员福利了。
  Cookie运行机制的基本原理与上述场景相似,客户端对应顾客,服务器端对应超市,Cookie对应会员卡。
  Cookie的创建和分发过程与超市会员卡场景中的会员卡发放相似。Cookie是通过扩展HTTP通信协议来实现的。Cookie是服务器端在响应客户端请求时,在HTTP的响应头中加入的特定数据信息。客户端浏览器在接收服务器端响应内容后,读取特定数据信息并存储。其中“在HTTP的响应头中加入特定数据信息”需要开发人员编写代码实现,“客户端浏览器读取特定数据信息并存储”与开发人员无关,浏览器具备这个功能。
  Cookie的使用过程与顾客去超市时携带并出示会员卡相似。客户端浏览器在访问网站时,会按照一定的规则自动把Cookie添加到请求中发送给服务器端。具体过程为:浏览器检查所有存储的Cookie,如果某个Cookie符合所请求网站的范围,则把该Cookie添加到HTTP请求头中发送给服务器端。类似于顾客有各种不同的会员卡,去“A超市”时要带上该超市能使用的会员卡。其中“按照一定的规则自动把Cookie添加到请求中发送给服务器端”与开发人员无关,浏览器具备这个功能。
  Java Web开发中,Cookie的工作原理如图5-2所示。服务器端Servlet程序创建Cookie并在响应中写入Cookie,客户端浏览器读取响应头中的Cookie并自动存储。在后续的请求中,浏览器自动在请求头中包含Cookie,服务器端Servlet可读取Cookie并进行相应的个性化业务处理和响应。
  

图5-2  Cookie的工作原理
  Cookie的内容主要包括:名字、值、到期时间、路径和域。域用于指定某一个特定域名,如baidu.com,也可以用于指定域名下的二级域名,如tieba.baidu.com或者fanyi.baidu.
com。路径就是域名后面的URL路径,如“/”或者“/admin”等。域与路径合在一起就构成了Cookie的作用范围。
  若不设置到期时间,则表示这个Cookie的生命周期为浏览器会话期间,只要关闭浏览器,Cookie就消失了。这种生命周期为浏览器会话期的Cookie被称为会话Cookie,会话Cookie不存储在客户端硬盘而是保存在浏览器运行时的内存中。
  若设置了到期时间,浏览器就会把Cookie存储在客户端硬盘上,关闭后再次打开浏览器,这些Cookie仍然有效,直到超过设定的到期时间。
  存储在客户端硬盘上的Cookie可以在同一个浏览器的不同进程间共享,如同一个客户端上开启的两个Google Chrome浏览器应用,但是不同浏览器存储的Cookie不能共享。
  而对于保存在浏览器运行时内存中的Cookie,当前浏览器所有进程和标签页都可以共享Cookie。
  使用浏览器内置的开发者工具,可以查看当前正在浏览网站的Cookie信息。以Google Chrome浏览器为例,访问百度网站,单击键盘上的F12快捷键,在弹出的界面上选择“网络”或者“Network”选项卡,然后单击左侧的www.baidu.com文件,在右侧查看响应标头中的Cookie信息,如图5-3所示。
  

图5-3  HTTP通信协议响应头中的Cookie信息
  在浏览器中通过“设置”菜单,可以查看浏览器存储在硬盘的所有网站的Cookie信息。以Google Chrome浏览器为例,单击“设置”菜单,在弹出的界面上单击“隐私设置和安全性”选项,继续选择“Cookie及其他网站数据”→“查看所有Cookie和网站数据”命令,在弹出的界面上单击“baidu.com”,即可以查看百度网站相关的Cookie信息,如图5-4所示。
  

图5-4  浏览器存储在硬盘的Cookie信息
  
* 5.2.3  Cookie的应用
  在Java Web开发中,开发人员在服务器端进行Cookie的创建、设置和发送等相关代码的编写。Cookie的常用方法如表5-1所示。
表5-1  Cookie的常用方法
方?法?名
说??明
setDomain
设置Cookie适用的域名
setMaxAge
设置Cookie过期时间,以秒为单位
setPath
设置能够访问 Cookie的路径
setSecure
设置浏览器使用的安全协议,如HTTPS
setValue
设置Cookie对象的值
  1.??Cookie的创建与发送
  通常在服务器端Servlet中编写代码创建Cookie,服务器端的一次响应过程中可以创建与发送多个Cookie对象,示例代码如下:
  
     String id = request.getParameter("id");          //假设id参数值为user01
     String name = request.getParameter("name");     //假设name参数值为name01
     Cookie cookieId = new Cookie("id",id);         //以键值对的方式存放内容
     Cookie cookieName = new Cookie("name",name);  //以键值对的方式存放内容
  
  Cookie对象创建后,不能增加其他的键值对,但是可以修改Cookie对象值的内容,示例代码如下:
     cookieName.setValue("userNameStr");             
  Cookie对象添加到response对象返回给客户端,示例代码如下:
  
     response.addCookie(cookieId);
     response.addCookie(cookieName);
  
  上述代码执行后,在浏览器内置的开发者工具中可以查看响应头中的Cookie信息,如图5-5所示。
  2.??Cookie过期时间的设置
  默认情况下,Cookie为会话Cookie,即Cookie保存在浏览器运行时的内存中,浏览器关闭后,Cookie就失效了。通过设置过期时间,告知浏览器把Cookie数据保存在客户端硬盘,即使关闭浏览器,Cookie仍然有效,示例代码如下:
  
     cookieName.setMaxAge(-1);       //参数为负整数,关闭浏览器,Cookie就失效
     cookieId.setMaxAge(7*24*60*60);//参数为正整数,单位为秒,Cookie存储到客户端硬盘
                                          //本语句表示Cookie在7天内有效
     cookieId.setMaxAge(0);           //参数为0,立即删除Cookie

图5-5  在浏览器中查看响应头中的Cookie信息
  3.??Cookie作用范围的设置
  Cookie默认的路径为当前访问的Servlet的父路径,默认作用范围是当前域名的默认路径。
  例如,在问卷调查网站中,创建和发送Cookie的是SendCookieServlet,路径为http://
localhost:8080/question/SendCookieServlet。由此创建的Cookie默认路径是/question,默认作用范围是http://localhost:8080/question。
  在单独网站的开发过程中,通常使用Cookie默认作用范围即可。若要在Tomcat中部署的所有网站都能使用同一个Cookie,需要设置该Cookie的路径为“/”,示例代码如下:
     cookie.setPath("/");             //当前Tomcat下所有网站都能获取该Cookie
  Cookie的作用范围支持跨域。例如,在百度(www.baidu.com)网站中创建的Cookie,需要在百度贴吧(teiba.baidu.com)、百度翻译(fanyi.baidu.com)等网站中有效,可通过设置Cookie的域实现,示例代码如下:
     cookie.setDomain(".baidu.com");   //参数以“.”开始,在一级域名下实现Cookie共享
  在Java Web项目开发中,通常包含用户登录功能模块,用户登录时会提供“记住我”选项,让登录页面记住用户上一次访问时的用户名,使得网站的用户体验更佳。这项功能可以使用Cookie技术实现。
实例5-1  利用Cookie实现记住用户名的功能
  本实例为在网站登录页面login.html中使用JavaScript代码读取Cookie信息,实现“记住我”功能,用户在7天内访问该页面时,页面自动填写用户名,浏览效果如图5-6所示。
  

图5-6  利用Cookie实现记住用户名的浏览效果
源程序:index.jsp文件
     <%@ page contentType="text/html;charset=UTF-8" language="java" %>
     <!DOCTYPE html>
     <html>
     <head>
         <title>Cookie应用</title>
     </head>
     <body>
     <h1>欢迎来到Java Web开发的网站</h1>
     <a href="<%=request.getContextPath()%>/login.html">重新登录</a>
     </body>
     </html>
源程序:login.html文件
     <!DOCTYPE html>
     <html lang="en">
     <head>
         <meta charset="UTF-8">
         <title>Cookie应用</title>
     </head>
     <body>
     <form action="DoLoginServlet" method="post">
       用户名称:<input type="text"  name="username" id="username" /><br>
       用户密码:<input type="password" name="password" /><br>
       记住我:<input type="checkbox" name="remember" value="ok" /><br>
       <input type="submit" value="登录"/>
     </form>
     
     <script>
       //调用函数getCookieValueByKey获取Cookie中保存的用户名称
       //注意:调用函数中参数的值,与Servlet中创建的Cookie的key要一致
       let cookieUserName = getCookieValueByKey("name");
       //根据HTML标记的id属性,获取用户名称文本框,并写入Cookie中读取的用户名
       document.getElementById("username").value = cookieUserName;
     
       //函数:根据Cookie的key获取对应的值
       function getCookieValueByKey(key){
         let val = "";
         let cookies = document.cookie;
         cookies = cookies.replace(/\s/,"");
         let cookieArray = cookies.split(";");
         for(let i=0; i<cookieArray.length; i++){
           let cookie = cookieArray[i];
           let kvArray = cookie.split("=");
           if(kvArray[0] == key){
             val = kvArray[1];
             break;
           }
         }
         return val;
       }
     </script>
     </body>
     </html>
源程序:DoLoginServlet.java文件的部分代码
     @WebServlet("/DoLoginServlet")
     public class DoLoginServlet extends HttpServlet {
         @Override
         protected void doPost(HttpServletRequest request, 
             HttpServletResponse response) throws ServletException, IOException{
             //获取表单提交的三个参数
             //注意:request.getParameter方法参数值与表单中各个标记的name值应当一致
             String username = request.getParameter("username");
             String password = request.getParameter("password");
             String remember = request.getParameter("remember");
             //当用户选中“记住我”选项提交时,remember变量值为ok
             if("ok".equals(remember)) {
                 Cookie cookie = new Cookie("name", username); //Cookie的key为name
                 cookie.setMaxAge(7 * 24 * 60 * 60); 		//Cookie有效期为7天
                 response.addCookie(cookie);
             }
             //这里假设正确的用户名称和用户密码分别是admin和pwd
             //实际项目中,通常与数据库中的用户名称和用户密码进行比较
             if ("admin".equals(username) && "pwd".equals(password)){
                 //验证通过,重定向至首页
                 response.sendRedirect(request.getContextPath() + "/index.jsp");
             }else{
                 //验证失败,重定向至登录页面
                 response.sendRedirect(request.getContextPath() + "/login.html");
             }
         }
     }
  
  操作步骤:
  (1)在Book项目中创建ch05模块。右击Book项目名称,在弹出的快捷菜单中选择New→Module命令,在弹出的对话框中选择模块类型为Java Enterprise,输入模块名称为ch05、位置为D:\ideaProj\Book\ch05,选择模块结构为Web application,选择应用服务器为Tomcat 9.0.29,然后单击Next按钮,在弹出的对话框中单击Finish按钮,ch05模块建立      完成。
  (2)右击ch05模块中的webapp文件夹,在弹出的快捷菜单中选择New→JSP/JSPX命令,输入文件名index.jsp,按Enter键确认建立文件。
  (3)右击ch05模块中的webapp文件夹,在弹出的快捷菜单中选择New→HTML命        令,输入文件名login.html,按Enter键确认建立文件。
  (4)右击com.example.ch05包,在弹出的快捷菜单中选择New→Servlet命令,然后输入名称DoLoginServlet,按Enter键确认建立文件。
  (5)分别在三个新建的文件中输入源代码。
  (6)在IDEA运行工具栏中选择配置Tomcat,在Deployment选项卡内点击“+”按钮,选择Artifact选项,在弹出的界面中选择ch05:war exploded,最后修改Application context为“/ch05”。单击OK按钮,完成网站部署。
  (7)单击运行工具栏中的“运行”按钮,启动Tomcat。
  (8)在login.html代码视图中单击浏览器图标,或者在浏览器地址栏中输入网址http://localhost:8080/ch05/login.html,查看浏览效果。
  程序说明:
* 第一次登录时输入正确的用户名称admin和密码pwd,并且选中“记住我”,登录成功后,关闭浏览器。然后再打开浏览器,输入网址http://localhost:8080/ch05/ login.html,查看页面是否已经自动填写用户名称admin。
* 浏览本实例的过程中,通过浏览器内置的开发者工具,可以查看请求头中的Cookie信息。
* 本实例的Cookie有效期为7天,可以通过浏览器“设置”→“隐私设置和安全性”查看保存在硬盘localhost站点的Cookie详细信息。
* 操作步骤中的第(6)步,仅需要在ch05模块网站第一次运行时配置。
  
?5.3
  

Session技术
* 5.3.1  Session概述
  HTTP通信协议是一种无状态协议,即每次服务器端接收到客户端请求时,都是一个全新的请求,服务器端并不知道客户端的历史请求记录。
  Session是设计交互式动态网页的一项重要技术,它可以将客户端的用户数据存储在服务器端。通过在服务器端存储与客户端在同一个会话期间的交互信息,从而实现交互式动态网站。
  Cookie技术采用的是在客户端保持会话状态的方案,而Session技术采用的是在服务器端保持会话状态的方案。Session的主要目的就是为了弥补HTTP通信协议的无状态        特性。
* 5.3.2  Session的运行机制
  Session在客户端第一次访问服务器(Tomcat)时自动创建,但是只有访问Servlet、JSP等资源时才会创建Session,而访问HTML、CSS、图片等静态资源时并不会创建Session。
  当多个客户端访问服务器端时,服务器端会为每个客户端分别创建不同的Session,这些Session通过Id属性进行区分。服务器端依据客户端提供的Id来“识别”不同客户,读取相应Session的内容,保持与客户的会话状态。
  在Java Web开发中,Tomcat管理服务器端Session,在创建Session时,会自动创建名称为JSESSIONID的Cookie,并将Session的Id属性值赋值该Cookie,返回给客户端;客户端浏览器会在后续请求中,自动包含该Cookie。因此,服务器端在与客户端交互过程中,可依据Cookie中的JSESSIONID读取保存在服务器端的Session数据。
  由图5-7可以看出,服务器端存储了多个客户端的Session数据,识别当前客户端的Session依据的是客户端请求中包含的Cookie中JSESSIONID的值。JSESSIONID的值与Session的Id属性值是一一对应的。
  

图5-7  Session的工作原理
  由图5-2所示的Cookie的工作原理可知,普通的Cookie创建并添加到response响应中的工作,是开发人员在Servlet中编写代码实现的。但是JSESSIONID这个Cookie不需要开发人员编写代码实现,而是由Tomcat自动管理。
  通过Session技术维护服务器端与客户端的会话状态,需要Cookie技术的支持,当客户端浏览器禁用Cookie时,Tomcat会启用URL重写机制。这个机制是客户端Cookie被禁用时的一个兜底策略,通过在URL后面加上“;jsessionid=xxx”来传递Session的Id,这样即使Cookie被禁用时,也可以保证Session的可用性,但是Session的Id暴露在URL里,本身是不安全的。
  默认情况下,用户可以参考图?5-3,查看?Cookie?中的?JSESSIONID?的信息;浏览器 Cookie被禁用时,用户可以在浏览器地址栏中查看JSESSIONID的信息。
* 5.3.3  Session的生命周期
  Session数据是保存在服务器端的,为了获得更快的存取速度,服务器端一般把Session存储在内存里。每个客户端都有一个独立的Session,如果Session内容过于复杂,当大量