第9章 会话控制¶
第 9 章¶
会话控制¶
本章介绍会话控制,涉及两个会话技术:一个是用于客户端的 Cookie,另一个是用于服务器端的 Session。Cookie 是在浏览器中记录一些数据,并且浏览器在访问相同服务器端时会主动携带该数据。如果 Cookie 丢失,或者换了一个浏览器再访问,那么之前存储的 Cookie 数据就不存在了。Cookie 主要用于在客户端保持状态。而 Session 是在服务器端记录相关状态数据,并为每个用户生成一个唯一的 SessionID,将该 SessionID 存储在用户的 Cookie 中。相对于 Cookie,Session 可以存储更大量的数据,并且数据相对更安全,因为用户无法直接修改 Session 数据。需要注意的是,Session 依赖于 Cookie 来实现会话的唯一标识。本章将详细介绍 Cookie 和 Session 的应用案例、工作机制以及时效性等内容。
9.1 会话控制简介¶
由于 HTTP 是基于 TCP 协议的短连接,即完成一次 “请求 - 应答” 之后会断开连接。服务器端接到一

次 HTTP 请求时,不知道之前是否曾经收到过同一个客户端发送来的请求,即 “无状态”,如图 9-1 所示。这意味着如果服务器端处理请求时需要上次请求的信息,客户端必须重传全部信息,这样可能导致每次连接传送的数据量剧增。
例如,当用户登录某个邮箱系统后,可以在其中完成查看邮件、收信、发信等操作,这些操作有可能需要访问多个页面来完成,但每次访问新页面就要重新发送一次新的请求,意味着每次操作都需要登录,这样一来,就增加了很多不必要的麻烦。希望有一个技术可以帮我们实现在同一用户访问各个页面时,只需登录一次便能一直保持登录状态,该技术即本章将要学习的会话技术。
会话控制是一种面向连接的可靠通信方式,通常根据会话控制记录判断用户登录的行为。一次会话,是指从浏览器开启到浏览器关闭的整个过程,在此期间,浏览器和服务器端之间会发生连续的一系列请求和响应,就像是从拨通电话到挂断电话之间聊天的全过程。Web 应用的会话状态是指服务器端与浏览器在会话过程中产生的状态信息。借助会话状态,Web 服务器端能够把属于同一会话中的一系列请求和响应过程关联起来,使它们之间可以相互依赖和传递信息。例如,在一个购物网购物结算时,必须知道登录请求表单的结果,以便知道是哪个账户在操作。还必须知道已选商品的信息。其中的用户登录的账户信息和已选商品信息就是会话的状态信息。
而会话技术就是用来保存在会话期间,浏览器和服务器端所产生的数据。因此,我们可以把会话技术分为两类,一类是客户端的会话技术,实现把会话数据保存在客户端的操作,如 Cookie 技术,Cookie 是通过 HTTP 扩展实现的,即在 HTTP 请求头里增加 Cookie 字段,用于存储客户端信息。另一类是服务端的会话技术,实现把会话数据保存在服务端的操作,如 Session 技术。Session 是基于 Cooke 的,在 Cookie 基础上做了进一步完善,解决了 Cookie 的一些局限问题。
9.2 域对象的范围¶
Servlet 包括三类域对象:请求域、会话域和应用域。前面已经介绍了请求域和应用域,本章介绍的就是会话域,包括 Cookie 对象和 Session 对象。回顾之前知识,对比这三类域对象的作用范围,具体如下。
整个项目部署之后,只会有一个应用域对象,所有客户端都是共同访问同一个应用域对象,在该项目的所有动态资源中也是共用一个应用域对象。应用域的作用范围如图 9-2 所示。
对于请求域,每一次请求都有一个请求域对象,当请求结束的时候,对应的请求域对象也就销毁了。请求域的作用范围如图 9-3 所示。


会话域是从客户端连接上服务器端开始,一直到客户端关闭,整个过程中发生的所有请求都在同一个会话域中;而不同的客户端是不能共用会话域的。会话域的作用范围如图 9-4 所示。

介绍完会话控制、会话技术、会话域等概念,接下来正式进入本章内容,Cookie 技术和 Session 技术的介绍,包括二者是如何工作的、有什么区别,以及应用场景有哪些等。
9.3 Cookie 技术¶
Cookie 是一种客户端的会话技术,实际上是服务器端保存在浏览器上的一段信息,浏览器每次访问该服务器端的时候,都会携带 Cookie。由于 HTTP 是无状态协议,服务器端不能记录浏览器的访问状态,也就是说服务器端不能区分两次请求是否由一个客户端发出。Cookie 的出现完美解决了这个问题,浏览器访问服务器端时,可以将其访问状态记录在携带的 Cookie 中,从而根据 Cookie 就可以判断不同请求是否为同一客户端发出的。
Cookie 的主要作用就是在浏览器中存放数据。浏览器有了 Cookie 后,每次向服务器端发送请求时都会同时将该信息发送给服务器端,服务器端收到请求后,就可以根据该消息处理请求。
Cookie 可以用于保持用户的登录状态、记住用户名,以及保存电影的播放进度等方面。
Cookie 实际上是在浏览器端存储的一小段数据。当浏览器首次访问服务器端时,服务器端可以在响应头中添加 Set-Cookie 字段来设置 Cookie 的值,并将其发送给浏览器,浏览器接收到该头信息后,会将 Cookie 的信息保存,接下来浏览器每次访问服务器端时,会以请求头的形式再将该 Cookie 发送给服务器端,服务器端便可以通过不同的 Cookie 来区分不同的用户。上述流程就是 Cookie 的工作机制。
9.3.1 常用方法¶
了解了 Cookie 的作用和工作原理后,接下来继续介绍 Cookie 如何进行应用。常用方法包括 Cookie 对象的创建、获取 Cookie 的值等。另外,还涉及 HttpServletRequest 和 HttpServletResponse 对象对 Cookie 对象的相关操作。
- 创建一个 Cookie 对象,代码如下所示。
注意,Cookie 存储的是键值对,只能保存字符串数据。
- 通过 HttpServletRequest 对象将 Cookie 写回给浏览器端,代码如下。
- 通过 HttpServletRequest 对象获取浏览器携带的所有 Cookie,代码如下。
注意,得到所有的 Cookie 对象是一个数组,可以根据不同 key 值得到目标 Cookie 对象。
- 获取 Cookie 的名称、value 值,代码如下。
9.3.2 入门案例¶
下面通过案例演示 Cookie 的具体应用。创建 chapter09_cookie 模块,添加 Web 框架,并创建 ServletDemo01 类和 ServletDemo02 类,借助 Cookie 对象实现在会话域范围内共享数据。
创建 ServletDemo01 类继承 HttpServlet,重写 doGet () 和 doPost () 方法,创建 Cookie 数据并响应给客户端,示例代码如下。
创建 ServletDemo02 类继承 HttpServlet,重写 doGet () 和 doPost () 方法,获取 Cookie 数据,示例代码如下。
然后在 index.html 文件中编写超链接,访问 ServletDemo01 和 ServletDemo02 类,实现两类之间共享数据。
启动项目后,先打开 F12,再单击访问 ServletDemo01,由于没有设置响应内容,浏览器是一个空白页面,主要查看响应头携带的 Cookie 信息,如图 9-5 所示。
浏览器发送请求携带 Cookie, 这里不需要我们手动操作,浏览器会在给服务器端发送请求时,将 Cookie 通过请求头自动携带到服务器端。返回到首页,在 F12 开启的情况下,然后单击访问 ServletDemo02, 如图 9-6 所示,可以发现请求头中包含了 cookie-message 的信息。查看控制台,如图 9-7 所示,成功获取 Cookie 信息。
图 9-5 查看响应头携带的 Cookie 信息
| 名称 | 标头 预览 响应 发起程序 计时 Cookie |
| servletDemo01 | 响应头 查看源 Content-Length: 0 Date: Sun, 12 Feb 2023 12:45:42 GMT Set-Cookie: cookie-message=hello-cookie |
| 请求标头 查看源 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 Cache-Control: no-cache Connection: keep-alive | |
| 1次请求 已传输114 B |


9.3.3 有效时间¶
默认情况下,Cookie 的有效期是一次会话范围内,我们还可以通过 Cookie 的 setMaxAge () 方法设置其时效性,保证 Cookie 持久化保存到浏览器上。
对于会话级别的 Cookie,服务器端并没有明确指定 Cookie 的存在时间。在浏览器端,Cookie 数据存在于内存中,只要浏览器处于打开状态,Cookie 数据就会一直都存在,当浏览器关闭时,内存中的 Cookie 数据就会被释放。
而持久化后的 Cookie,服务器端明确设置了其存在的时间。在浏览器端,Cookie 数据会被保存到硬盘上,Cookie 在硬盘上存在的时间根据服务器端限定的时间来管控,不受浏览器关闭的影响,直到持久化 Cookie 到达预设的时间才会被释放。
设置 Cookie 持久化的代码格式如下。
cookiesetMaxAge (int expiry); // 设置 cookie 的最长有效时间
参数单位是秒,表示 Cookie 的持久化时间。一旦设置了有效时间,时间一到 Cookie 就会自动消失,与浏览器是否关闭无关。值得注意的是,如果参数设置为 0,表示删除浏览器中保存的 Cookie 数据。
例如,创建 CookieTestServlet 类继承 HttpServlet,在该类中创建 Cookie 对象,并为该 Cookie 对象设置有效时间,示例代码如下。
在 index.html 文件中创建超链接访问类,示例代码如下。
启动项目,在首页单击 F12 键,并单击访问 CookiePathTestServlet,查看响应头信息,如图 9-8 所示。可知 “username=xiaoShang” 的 Cookie 对象设置成功,在请求头中包含该 Cookie 对象,且该对象的有效时间设置为 60 秒。60 秒后,再次单击超链接,访问 CookiePathTestServlet,查看请求头信息,如图 9-9 所示。可知 “username=xiaoShang” 的 Cookie 对象已失效。
图 9-8 查看 Cookie 对象的有效时间
| 名称 cookieTestServlet | × 标头 预览 响应 发起程序 计时 Cookie | |
| 响应头 Content-Length: 7 Date: Sun, 12 Feb 2023 13:09:53 GMT Set-Cookie: username=xiaoShang; Max-Age=60; Expires=Sun, 12-Feb-2023 13:10:53 GMT | ||
| 请求标头 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 Cache-Control: no-cache Connection: keep-alive Cookie: cookie-message=hello-cookie; username=xiaoShang Host: localhost:8080 | ||
图 9-9 60 秒后,再次访问 CookiePathTestServlet
| 名称 cookieTestServlet | X 标头 预览 响应 发起程序 计时 Cookie 引用者策略: strict-origin-when-cross-origin ▼响应头 查看源 Content-Length: 7 Date: Sun, 12 Feb 2023 13:13:13 GMT Set-Cookie: username=xiaoShang; Max-Age=60; Expires=Sun, 12-Feb-2023 13:14:13 GMT ▼请求标头 查看源 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 Cache-Control: no-cache Connection: keep-alive Cookie: cookie-message=hello-cookie Host: localhost:8080 |
9.3.4 路径¶
一般情况下,上网时间长了,本地会自动保存很多 Cookie 数据。但对浏览器来说,不可能每次访问互联网资源时,都携带所有 Cookie 数据。那么如何区分需要携带的数据呢?浏览器会使用 Cookie 的 path 属性值来和当前访问的地址进行比较,从而决定是否携带 Cookie。
我们可以通过调用 Cookie 的 setPath () 方法来设置 Cookie 的 path 属性,代码格式如下。
其中,浏览器发送请求时,会根据 path 路径判断需要携带哪些 Cookie 给服务器端。
例如,创建 CookiePathTestServlet 类继承 HttpServlet,然后在该类中创建 Cookie 对象,并为其设置 path 路径,示例代码如下。
在 index.html 文件中,创建超链接,访问 CookiePathTestServlet 类,示例代码如下。
启动项目,在首页单击 F12 键,并单击访问 CookiePathTestServlet,查看响应头信息,如图 9-10 所示。可知 “password=atguigu” 的 Cookie 对象对应的 path 路径设置成功,并且由于当前访问该 path 路径,可以看到请求头中只携带了 “password=atguigu” 的 Cookie 对象。
另外,Cookie 是通过明文传送的,安全性较差,作为请求或响应报文发送,无形中也增加了网络流量。并且 Cookie 信息存储在浏览器中,数量也是有局限的。由于 Cookie 的限制性,注定不能在 Cookie 中保存过多的信息,于是 Session 出现了。Session 是服务器端的技术,服务器端为每一个浏览器开辟一块内存空间,存放 Session 对象。Session 的作用就是在服务器端保存一些用户的数据。

9.4 Session 技术¶
由于 Cookie 中的信息是保存在浏览器端的,浏览器每次访问时都需要发回 Cookie,因此我们不能在 Cookie 中保存大量的信息。既然客户端不能保存大量的信息,那么可不可以将信息保存到服务器端呢?答案是肯定的,我们可以为每次会话在服务器端中创建一个对象,然后在该对象中保存相关的信息。这个对象的类型是 HttpSession,后面提到的 Session 对象就是 HttpSession 类型的对象。那么如何在会话和对象之间建立一个对应关系呢?服务器端会给每个 Session 对象都赋予一个 id 值,这个 id 是不可重复的,也就是说每个 Session 对象都有唯一的标识。这样我们就可以将这个唯一的标识交给浏览器保存,浏览器每次访问服务器时都会带着这个唯一标识,这样服务器就可以根据这个唯一标识找到每个会话对应的 Session 对象了。这个唯一标识被称为 JSESSIONID,保存在浏览器的 Cookie 中,因此 HttpSession 运行时依赖于 Cookie。
因为 Session 对象是每个浏览器特有的,所以用户的记录可以存放在 Session 对象中,然后传递给用户一个名字为 JSESSIONID 的 Cookie,这个 JSESSIONID 对应这个服务器中的一个 Session 对象,通过它就可以获取到保存用户信息的 Session 对象。
Session 的工作机制如下。Session 对象就相当于浏览器在服务器的账户,而 Session 对象的 id (JSESSIONID),相当于这个账户的账号。实际上 Session 对象就是服务器中用来保存会话信息的对象,每个 Session 对象都有唯一的 id,这个 id 通过 Cookie 的形式先由服务器发送给浏览器,浏览器收到 Cookie 后会自动保存,然后在每次访问服务器时,都会带着这个 Cookie,服务器就可以根据 Cookie 中保存的 Session 的 id 找到浏览器对应的 Session 对象。
当服务器端第一次创建 Session 对象的时候,会话就开始了,会话结束分为以下三种情况。
(1)名字为 JSESSIONID 的 Cookie 消失时,即浏览器关闭。
名字为 JSESSIONID 的 Cookie 是一个瞬时的 Cookie,当它消失了,对于服务器来说,意味着找不到 Session 对象,此时服务器会重新创建 Session 对象,从而得到一个全新的 Cookie,这样一来,与之前 Cookie 的会话就结束了。
(2)服务器强制将 Session 对象销毁。
当服务器强制将 Session 对象销毁后,即使客户端还存在 JSESSIONID,但根据 JSESSIONID 找不到对应的 Session 对象,服务器仍会重新创建 Session 对象,重新分配给客户端一个 Cookie,并覆盖掉之前的 Cookie。
(3) Session 的自动失效机制。
一旦 Session 自动失效,和服务器强制销毁是一个效果,即使客户端有 JSESSIONID,但根据 JSESSIONID 找不到对应的 Session 对象,服务器仍会重新创建 Session 对象,重新分配给客户端一个 Cookie,并覆盖掉之前的 Cookie。
9.4.1 入门案例¶
下面同样通过案例演示 Session 的应用。在会话域范围内,借助 Session 实现在 ServletDemo03 和 ServletDemo04 之间共享数据。
Session 的常用方法如下。
创建 chapter09_session 模块,在该模块下创建 ServletDemo03 类继承 HttpServlet, 重写 doGet () 和 doPost () 方法,创建 HttpSession 对象并设置其属性,示例代码如下。
创建 ServletDemo04 类继承 HttpServlet,重写 doGet () 和 doPost () 方法,获取 Session 属性,示例代码如下。
同样,在 index.html 文件中编写超链接,访问 ServletDemo03 和 ServletDemo04 类,实现两类之间共享数据。接下来启动项目,首先单击访问 ServletDemo03,创建 Session 对象,查看控制台,如图 9-11 所示。

在网页上单击 F12 键,查看响应头中的 JSESSIONID,如图 9-12 所示。

然后访问 ServletDemo04,获取 Session 对象及设置的属性值,查看控制台,如图 9-13 所示。

结果表明,ServletDemo04 类中获取的 Session 和 ServletDemo03 类中创建的是同一个对象,且在两个类之间成功传递了 “session-message” 的属性值。
再次在网页单击 F12 键,查看 ServletDemo04 的 Cookie 信息,如图 9-14 所示。

可以看出,这里同样携带了与 ServletDemo03 中相同的 JSESSIONID。再次论证了每个浏览器对应唯一的 Session 对象,而每个 Session 对象又对应唯一的 JSESSIONID。
9.4.2 工作机制¶
在浏览器正常访问服务器的前提下,当服务器端调用了 request.getSession () 方法,服务器端会检查当前请求中是否携带了 JSESSIONID 的 Cookie。分两种情况:如果有,则根据 JSESSIONID 在服务器端查找对应的 HttpSession 对象,能够找到的话,将找到的 HttpSession 对象作为 request.getSession () 方法的返回值返回;找不到,则服务器端会新建一个 HttpSession 对象作为 request.getSession () 方法的返回值返回。
如果没有,服务器端会直接新建一个 HttpSession 对象作为 request.getSession () 方法的返回值返回,如图 9-15 所示。

另外,可以通过如下代码,查看 HttpSession 对象是否为新对象,以及获取 HttpSession 对象的 id 值。
// 1. 调用 request 对象的方法尝试获取 HttpSession 对象
// 2. 调用 HttpSession 对象的 isNew () 方法
// 3. 打印 HttpSession 对象是否为新对象
// 4. 调用 HttpSession 对象的 getId () 方法
// 5. 打印 JSESSIONID 的值
9.4.3 有效时间¶
当浏览器的用户访问量较大时,Session 对象相应的也要创建很多。如果一味创建不释放,那么服务器端的内存迟早要被耗尽。因此我们要为 Session 设置时限。
不过困难之处在于,从服务器端角度来看,很难精确得知类似浏览器关闭的动作。而且即使浏览器一直没有关闭,也不代表用户仍然在使用。因此,决定让服务器端给 Session 对象设置最大闲置时间,服务器端给 Session 对象设置最大闲置时间的默认值为 1800 秒。自行设置具体时间的代码格式如下。
void setMaxInactiveInterval (int var1); // 设置最大闲置时间,单位是 s (秒)
最大闲置时间生效的机制如图 9-16 所示。

测试 Session 时效性的示例代码如下。
// 获取默认的最大闲置时间
// 设置默认的最大闲置时间
前面提到,我们还可以直接强制 Session 立即失效,代码格式如下。
9.5 案例:登录功能完善¶
下面借助会话控制技术完善 chapter07_login_register 项目的登录功能,使用 Cookie 实现记住用户名和密码功能,使用 Session 实现保持登录状态功能。
创建 chapter09_login_register 模块,并复制 chapter07_login_register 用户登录注册项目,在此基础上实现以下代码。
修改 LoginServlet 类,示例代码如下。将 phone 和 password 存储在 Cookie 内,设置时间默认是 30 日,然后将 Cookie 添加到 HttpServletResponse 中,同时将用户信息保存到 Session,保持登录状态。