3.8 会话
HTTP的设计天然是无状态协议。为了构建有效的Web应用程序,必须将来自特定客户端的请求彼此关联,因此衍生出了很多会话(Session)跟踪机制。
然而,直接使用这些会话跟踪机制是困难或麻烦的。因此,Servlet规范定义了一个简单的HttpSession接口,该接口允许Servlet容器使用多种方法来跟踪用户的会话,而无须让应用开发者感觉到使用这些方法的差别。
3.8.1 会话跟踪机制
会话跟踪机制有以下几类:
1.Cookie
通过HTTP Cookie进行会话跟踪是常用的会话跟踪机制,要求被所有Servlet容器支持。
容器向客户端发送一个Cookie。然后,客户端会在后续每个请求上携带该Cookie,并返回给服务器,这样就明确地每个请求与会话关联关系。会话是通过Cookie的标准名称来跟踪的。Cookie的标准名称必须是JSESSIONID.Containers格式,允许通过容器特定配置来定制Cookie的名称。
所有Servlet容器必须提供配置容器是否将会话跟踪Cookie标记为HttpOnly的能力。已建立的配置必须适用于尚未建立上下文特定配置的所有上下文。
如果Web应用程序为其会话跟踪Cookie配置自定义名称,而会话ID在URL中进行了编码(前提是已启用URL重写),那么同样的自定义名称也将用作URI参数的名称。
2.SSL会话
安全套接字层是HTTPS协议中使用的加密技术,它有一个内置的机制,可以允许客户端的多个请求被明确地标识为会话的一部分。Servlet容器可以很容易地使用这些数据来定义会话。
3.URL重写
URL重写是会话跟踪的最小公分母。当客户端不接受Cookie时,URL重写可能被服务器用作会话跟踪的基础。URL重写涉及将数据(一个会话ID)添加到由容器解释的URL路径,以将请求与会话关联起来。
会话ID必须编码为URL字符串中的路径参数。参数的名称必须是jsessionid。下面是一个包含编码路径信息的URL示例:
http://waylau.com/catalog/index.html;jsessionid=1234
URL重写在日志、书签、引用标头、缓存的HTML和URL栏中公开会话标识符。URL重写不应该被用作一个会话跟踪机制,在这个机制中,Cookie或SSL会话是受支持的和合适的。
4.会话的完整性
Web容器必须能够支持HTTP会话,同时为不支持使用Cookie的客户端提供HTTP请求。为了满足这个需求,Web容器通常支持URL重写机制。
3.8.2 创建会话
当会话只是一个预期的会话且尚未建立时,它会被认为是“新”的。因为HTTP是基于请求-响应的协议,所以在客户端“加入”之前HTTP会话被认为是新的。当会话跟踪信息被返回到服务器,表明已经建立了会话时,客户端加入会话。在客户端加入会话之前,不能假定客户端的下一个请求将被视为会话的一部分。
如果下列任何一项是正确的,会话就会被认为是新的:
- 客户端还不了解会话。
- 客户端选择不加入一个会话。
如果会话被认为是“新”的,则证明这个“新”会话是没有跟之前的请求有任何关联关系的。
Servlet开发人员必须设计应用程序来处理客户端没有、不能或不会加入会话的情况。
与每个会话相关联,有一个包含一个独特标识符的字符串,被称为会话ID。会话ID的值可以通过调用javax.servlet.http.HttpSession.getId()获取,创建后可以通过调用javax.servlet.http.HttpServletRequest.changeSessionId()改变。
3.8.3 会话范围
HttpSession对象必须在应用程序(或Servlet上下文)级别范围内。在底层机制,例如用于建立会话的Cookie,对于不同的上下文可以是相同的,但是引用的对象(包括该对象中的属性)不能被容器在上下文之间共享。
比如,如果一个Servlet使用RequestDispatcher在另一个Web应用程序中调用Servlet,那么为这个Servlet创建并可见的任何会话都必须与调用Servlet可见的会话不同。
此外,上下文的会话必须可被请求恢复到该上下文,无论它们的关联上下文是否被直接访问或在创建会话时作为请求分派的目标。
3.8.4 绑定属性到会话
Servlet可以通过名称将对象属性绑定到HttpSession实现。任何绑定到会话的对象都可用于属于同一个ServletContext的任何其他Servlet,并处理被标识为同一会话的一部分请求。
当某些对象被放置或从会话中删除时,可能需要通知。此信息可以通过对象实现HttpSessionBindingListener接口获得。该接口定义了以下方法,这些方法将指示被绑定到的对象或从会话中释放的对象。
- valueBound
- valueUnbound
在通过HttpSession接口的getAttribute方法提供对象之前,必须调用valueBound方法。在对象不再通过HttpSession接口的getAttribute方法可达之后,必须调用valueUnbound方法。
3.8.5 会话超时
在HTTP协议中,当客户端不再活动时,是没有显式地终止信号的。这意味着唯一可以用来指示客户端不再活动的机制是超时时间。
会话的默认超时时间由Servlet容器定义,可以通过ServletContext接口的getSessionTimeout方法或HttpSession接口的getMaxInactiveInterval方法获得。
这个时间可以由开发人员使用ServletContext接口的setSessionTimeout方法或HttpSession接口的setMaxInactiveInterval方法来更改。会话超时方法使用的超时时间定义为分钟。setMaxInactiveInterval方法所使用的超时时间定义为秒。根据定义,如果会话的超时时间设置为0或更低的值,会话就不会过期。在使用该会话的所有Servlet退出服务方法之前,会话将不会失效。一旦会话失效开始,新的请求就不能看到会话。
3.8.6 最后访问时间
HttpSession接口的getLastAccessedTime方法允许Servlet在当前请求之前确定会话上次访问的时间。当会话的一部分请求首先由Servlet容器处理时,会话被认为是被访问的。
3.8.7 线程问题
执行请求线程的多个Servlet可以同时对同一个会话对象进行活动访问。容器必须确保对表示会话属性的内部数据结构的操作以线程安全的方式执行。开发人员负责线程安全访问属性对象本身。这将保护HttpSession对象内的属性集合不受并发访问,从而消除了应用程序导致该集合被破坏的机会。除非在规范中有明确的声明,否则从请求或响应中获得的对象必须被假定为非线程安全的。这包括但不限于ServletResponse.getWriter()方法返回的PrintWriter和ServletResponse.getOutputStream()方法返回的OutputStream。
3.8.8 分布式环境
在分布式的应用程序中,所有属于会话的请求必须一次由一个JVM处理。容器必须能够正确地使用setAttribute或putValue方法对所有放置到HttpSession类实例中的对象进行处理。为了满足这些条件,我们实施了以下限制:
- 容器必须接受实现Serializable接口的对象。
- 容器可以选择支持HttpSession中其他指定对象的存储,例如对Enterprise JavaBeans组件和事务的引用。
- 迁移会话将由特定容器设施处理。
当分布式Servlet容器不支持必需的会话迁移存储对象机制时,容器必须抛出IllegalArgumentException。
分布式Servlet容器必须支持迁移的对象实现Serializable的必要机制。
这些限制意味着开发人员可以确保在非分布式容器中不存在其他并发性问题。
容器提供程序可以通过将会话对象及其内容(从分布式系统的任何活动节点)移动到系统的不同节点,从而确保负载均衡和故障转移等服务特性的可伸缩性和质量。
如果分布式容器持久化或迁移会话以提供服务特性的质量,那么它们并不局限于使用本机JVM序列化机制来序列化httpsession及其属性。开发人员不能保证容器会在会话属性上调用readObject和writeObject方法,如果实现了这些方法,那么保证它们的属性的可序列化闭包将被保留。
容器必须在迁移会话期间通知任何实现HttpSessionActivationListener的会话属性。它们必须在会话序列化之前通知侦听器,并在会话反序列化之后激活。编写分布式应用程序的开发人员应该意识到,由于容器可能在多个Java虚拟机中运行,因此开发人员不能依赖静态变量来存储应用程序状态,应该使用企业Bean或数据库存储这些状态。
3.8.9 客户端语义
由于Cookie或SSL证书通常由Web浏览器进程控制,且不与浏览器的任何特定窗口相关联,因此从客户端应用程序的所有窗口向Servlet容器请求的可能是同一会话的一部分。为了获得最大的可移植性,开发人员应该始终假设客户的所有窗口都参与了相同的会话。