第 8 章 Request Context之Session指南

8.1. Session概述
8.1.1. 什么是Session
8.1.2. Session数据存在哪?
8.1.3. 创建通用的session框架
8.2. Session框架
8.2.1. 最简配置
8.2.2. Session ID
8.2.3. Session的生命期
8.2.4. Session Store
8.2.5. Session Model
8.2.6. Session Interceptor
8.3. Cookie Store
8.3.1. 多值Cookie Store
8.3.2. 单值Cookie Store
8.4. 其它Session Store
8.4.1. Simple Memory Store
8.5. 本章总结

Webx实现了一套session框架。Session框架建立在request contexts机制之上。建议你先阅读第 6 章 Filter、Request Contexts和Pipeline第 7 章 Request Contexts功能指南,以便了解request contexts是怎么回事。

8.1. Session概述

8.1.1. 什么是Session

HTTP协议是无状态的,但通过session机制,就能把无状态的变成有状态的。Session的功能就是保存HTTP请求之间的状态数据。有了session的支持,就很容易实现诸如用户登录、购物车等网站功能。在Servlet API中,有一个HttpSession的接口。你可以这样使用它:

例 8.1. 在Java代码中访问session

在一个请求中,保存session的状态

// 取得session对象
HttpSession session = request.getSession();

// 在session中保存用户状态
session.setAttribute("loginId", "myName");

在另一个请求中,取出session的状态:

// 得到"myName"
String myName = (String) session.getAttribute("loginId");

8.1.2. Session数据存在哪?

Session的状态数据是怎样保存的呢?

8.1.2.1. 保存在应用服务器的内存中

一般的做法,是将session对象保存在内存里。同一时间,会有很多session被保存在服务器的内存里。由于内存是有限的,较好的服务器会把session对象的数据交换到文件中,以确保内存中的session数目保持在一个合理的范围内。

为了提高系统扩展性和可用性,我们会使用集群技术 —— 就是一组独立的机器共同运行同一个应用。对用户来讲,集群相当于一台“大型服务器”。而实际上,同一用户的两次请求可能被分配到两台不同的服务器上来处理。这样一来,怎样保证两次请求中存取的session值一致呢?

一种方法是使用session复制:当session的值被改变时,将它复制到其它机器上。这个方案又有两种具体的实现,一种是广播的方式。这种方式下,任何一台服务器都保存着所有服务器所接受到的session对象。服务器之间随时保持着同步,因而所有服务器都是等同的。可想而知,当访问量增大的时候,这种方式花费在广播session上的带宽有多大,而且随着机器增加,网络负担成指数级上升,不具备高度可扩展性。

另一种方法是TCP-Ring的方式,也就是把集群中所有的服务器看成一个环,A→B→C→D→A,首尾相接。把A的session复制到B,B的session复制到C,……,以此类推,最后一台服务器的session复制到A。这样,万一A宕机,还有B可以顶上来,用户的session数据不会轻易丢失。但这种方案也有缺点:一是配置复杂;二是每增添/减少一台机器时,ring都需要重新调整,这将成为性能瓶颈;三是要求前端的Load Balancer具有相当强的智能,才能将用户请求分发到正确的机器上。

8.1.2.2. 保存在单一数据源中

也可以将session保存在单一的数据源中,这个数据源可被集群中所有的机器所共享。这样一来,就不存在复制的问题了。

然而单一数据源的性能成了问题。每个用户请求,都需要访问后端的数据源(很可能是数据库)来存取用户的数据。

这种方案的第二个问题是:缺少应用服务厂商的支持 —— 很少有应用服务器直接支持这种方案。更不用说数据源有很多种(MySQL、Oracle、Hsqldb等各种数据库、专用的session server等)了。

第三个问题是:数据源成了系统的瓶颈,一但这个数据源崩溃,所有的应用都不可能正常运行了。

8.1.2.3. 保存在客户端

把session保存在客户端。这样一来,由于不需要在服务器上保存数据,每台服务器就变得独立,能够做到线性可扩展和极高的可用性。

具体怎么做呢?目前可用的方法,恐怕就是保存在cookie中了。但需要提醒的是,cookie具有有以下限制,因此不可无节制使用该方案:

  • Cookie数量和长度的限制。每个domain最多只能有20条cookie,每个cookie长度不能超过4KB,否则会被截掉。

  • 安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。即使加密也与事无补,因为拦截者并不需要知道cookie的意义,他只要原样转发cookie就可以达到目的了。

  • 有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。

虽然有上述缺点,但是对于其优点(极高的扩展性和可用性)来说,就显得微不足道。我们可以用下面的方法来回避上述的缺点:

  • 通过良好的编程,控制保存在cookie中的session对象的大小。

  • 通过加密和安全传输技术(SSL),减少cookie被破解的可能性。

  • 只在cookie中存放不敏感数据,即使被盗也不会有重大损失。

  • 控制cookie的生命期,使之不会永远有效。偷盗者很可能拿到一个过期的cookie。

8.1.2.4. 将客户端、服务器端组合的方案

任何一种session方案都有其优缺点。最好的方法是把它们结合起来。这样就可以弥补各自的缺点。

将大部分session数据保存在cookie中,将小部分关键和涉及安全的数据保存在服务器上。由于我们只把少量关键的信息保存在服务端,因而服务器的压力不会非常大。

在服务器上,单一的数据源比复制session的方案,更简单可靠。我们可以使用数据库来保存这部分session,也可以使用更廉价、更简单的存储,例如Berkeley DB就是一种不错的服务器存储方案。将session数据保存在cookie和Berkeley DB(或其它类似存储技术)中,就可以解决我们的绝大部分问题。

8.1.3. 创建通用的session框架

多数应用服务器并没有留出足够的余地,来让你自定义session的存储方案。纵使某个应用服务器提供了对外扩展的接口,可以自定义session的方案,我们也不大可能使用它。为什么呢?因为我们希望保留选择应用服务器软件的自由。

因此,最好的方案,不是在应用服务器上增加什么新功能,而是在WEB应用框架上做手术。一但我们在WEB应用框架中实现了这种灵活的session框架,那么我们的应用可以跑在任何标准的JavaEE应用服务器上。

除此之外,一个好的session框架还应该做到对应用程序透明。具体表现在:

  • 使用标准的HttpSession接口,而不是增加新的API。这样任何WEB应用,都可以轻易在两种不同的session机制之间切换。

  • 应用程序不需要知道session中的对象是被保存到了cookie中还是别的什么地方。

  • Session框架可以把同一个session中的不同的对象分别保存到不同的地方去,应用程序同样不需要关心这些。例如,把一般信息放到cookie中,关键信息放到Berkeley DB中。甚至同是cookie,也有持久和临时之分,有生命期长短之分。

Webx实现了这种session框架,把它建立在Request Contexts的基础上。

8.2. Session框架

8.2.1. 最简配置

例 8.2. Session框架基本配置(/WEB-INF/webx.xml

<services:request-contexts xmlns="http://www.alibaba.com/schema/services/request-contexts">
    <buffered />
    <lazy-commit />
    ...
    <session>
        <stores>
            <session-stores:simple-memory-store id="simple" /> 
        </stores>
        <store-mappings>
            <match name="*" store="simple" /> 
        </store-mappings>
    </session>
</services:request-contexts>

以上的配置,创建了一个最基本的session实现:将所有数据( name=*)保存在内存里( simple-memory-store)。

[警告]警告

最简配置只能用于开发,请不要将上述配置用在生产环境。因为simple-memory-store只是将数据保存在内存里。在生产环境中,内存有被耗尽的可能。这段配置也不支持服务器集群。

8.2.2. Session ID

Session ID唯一标识了一个session对象。把session ID保存在cookie里是最方便的。这样,凡是cookie值相同的所有的请求,就被看作是在同一个session中的请求。在servlet中,还可以把session ID编码到URL中。Session框架既支持把session ID保存在cookie中,也支持把session ID编码到URL中。

完整的session ID配置如下:

例 8.3. Session ID的配置

<session>
    <id cookieEnabled="true" urlEncodeEnabled="false">
        <cookie name="JSESSIONID" domain="" maxAge="0" path="/" httpOnly="true" secure="false" />
        <url-encode name="JSESSIONID" />
        <session-idgens:uuid-generator />
    </id>
</session>

上面这段配置包含了关于Session ID的所有配置以及默认值。如果不指定上述参数,则系统将使用默认值,其效果等同于上述配置。

表 8.1. Session ID的配置说明

配置<session><id> —— 将Session ID保存于何处?
cookieEnabled

是否把session ID保存在cookie中,如若不是,则只能保存的URL中。

默认为开启:true

urlEncodeEnabled

是否支持把session ID编码在URL中。如果为true开启,应用必须调用response.encodeURL()response.encodeRedirectURL()来将JSESSIONID编码到URL中。

默认为关闭:false

配置<session><id><cookie> —— 将Session ID存放于cookie的设置
name

Session ID cookie的名称。

默认为JSESSIONID

domain

Session ID cookie的domain。

默认为空,表示根据当前请求自动设置domain。这意味着浏览器认为你的cookie属于当前域名。如果你的应用包含多个子域名,例如:www.alibaba.comchina.alibaba.com,而你又希望它们能共享session的话,请把域名设置成“alibaba.com”。

maxAge

Session ID cookie的最长存活时间(秒)。

默认为0,表示临时cookie,随浏览器的关闭而消失。

path

Session ID cookie的path。

默认为/,表示根路径。

httpOnly

在session ID cookie上设置HttpOnly标记。

在IE6及更新版本中,可以缓解XSS攻击的危险。默认为true

secure

在session ID cookie上设置Secure标记。

这样,只有在https请求中才可访问该cookie。默认为false

配置<session><id><url-encode> —— 将Session ID编码到URL的设置
name

指定在URL中表示session ID的名字,默认也是JSESSIONID

此时,如果urlEncodeEnabledtrue的话,调用:

response.encodeURL("http://localhost:8080/test.jsp?id=1")

将得到类似这样的结果:

http://localhost:8080/test.jsp;JSESSIONID=xxxyyyzzz?id=1

配置<session><id><session-idgens:*> —— 如何生成session ID?
uuid-generator

以UUID作为新session ID的生成算法。

这是默认的session ID生成算法。

为了达到最大的兼容性,我们分两种情况来处理JSESSIONID

  1. 当一个新session到达时,假如cookie或URL中已然包含了JSESSIONID,那么我们将直接利用这个值。为什么这样做呢?因为这个JSESSIONID可能是由同一域名下的另一个不相关应用生成的。如果我们不由分说地将这个cookie覆盖掉,那么另一个应用的session就会丢失。

  2. 多数情况下,对于一个新session,应该是不包含JSESSIONID的。这时,我们需要利用SessionIDGenerator来生成一个唯一的字符串,作为JSESSIONID的值。SessionIDGenerator的默认实现UUIDGenerator

8.2.3. Session的生命期

所谓生命期,就是session从创建到失效的整个过程。其状态变迁如下图所示:

Session生命期

图 8.1. Session生命期

总结一下,其实很简单:

  1. 第一次打开浏览器时,JSESSIONID还不存在,或者存在由同一域名下的其它应用所设置的无效的JSESSIONID。这种情况下,session.isNew()返回true

  2. 随后,只要在规定的时间间隔内,以及cookie过期之前,每一次访问系统,都会使session得到更新。此时session.isNew()总是返回false。Session中的数据得到保持。

  3. 如果用户有一段时间不访问系统了,超过指定的时间,那么系统会清除所有的session内容,并将session看作是新的session。

  4. 用户可以调用session.invalidate()方法,直接清除所有的session内容。此后所有试图session.getAttribute()session.setAttribute()等操作,都会失败,得到IllegalStateException异常,直到下一个请求到来。

在session框架中,有一个重要的特殊对象,用来保存session生命期的状态。这个对象叫作session model。它被当作一个普通的对象存放在session中,但是通过HttpSession接口不能直接看到它。

关于session生命期的完整配置如下:

例 8.4. 关于Session生命期的配置

<session maxInactiveInterval="0" keepInTouch="false" forceExpirationPeriod="14400"
         modelKey="SESSION_MODEL">
    ...
</session>

参数的意思是:

表 8.2. Session生命期的配置参数

参数名说明
maxInactiveInterval

指定session不活动而失效的期限,单位是秒。

默认为0,也就是永不失效(除非cookie失效)。例如,设置3600秒,表示用户离开浏览器1小时以后再回来,session将重新开始,老数据将被丢弃。

keepInTouch

是否每次都touch session(即更新最近访问时间)。

如果是false,那么只在session值有改变时touch。当将session model保存在cookie中时,设为false可以减少网络流量。但如果session值长期不改变,由于最近访问时间一直无法更新,将会使session超过maxInactiveInterval所设定的秒数而失效。

默认为false

forceExpirationPeriod

指定session强制作废期限,单位是秒。

无论用户活动与否,从session创建之时算起,超过这个期限,session将被强制作废。这是一个安全选项:万一cookie被盗,过了这个期限的话,那么无论如何,被盗的cookie就没有用了。

默认为0,表示无期限。

modelKey

指定用于保存session状态的对象的名称。

默认为"SESSION_MODEL"。一般没必要修改这个值。

8.2.4. Session Store

Session Store是session框架中最核心的部分。Session框架最强大的部分就在于此。我们可以定义很多个session stores,让不同的session对象分别存放到不同的Session Store中。前面提到有一个特殊的对象“SESSION_MODEL”也必须保存在一个session store中。

Session和Stores

图 8.2. Session和Stores

类似于Servlet的配置,Session store的配置也包含两部分内容:session store的定义,和session store的映射(mapping)。

例 8.5. Session Store的配置

    <session>
        <stores>
            <session-stores:store id="store1" /> 
            <session-stores:store id="store2" /> 
            <session-stores:store id="store3" /> 
        </stores>
        <store-mappings>
            <match name="*" store="store1" /> 
            <match name="loginName" store="store2" /> 
            <matchRegex pattern="key.*" store="store3" /> 
        </store-mappings>
    </session>

定义session stores:你可以配置任意多个session store,只要ID不重复。此处,store1store2store3分别是三个session store的名称。

映射session stores:match标签用来精确匹配attribute name。一个特别的值是“*”,它代表默认匹配所有的names。

本例中, 如果调用session.setAttribute("loginName", user.getId()),那么这个值将被保存到store2里;如果调用session.setAttribute("other", value)将被默认匹配到store1中。

映射session stores:matchRegexp标签用正则表达式来匹配attribute names。

本例中, key_akey_b等值都将被保存到store3里。

需要注意以下几点:

  • 在整个session配置中,只能有一个store拥有默认的匹配

  • 假如有多个matchmatchRegex同时匹配某个attribute name,那么遵循以下匹配顺序:

    1. 精确的匹配最优先。

    2. 正则表达式的匹配遵循最大匹配的原则,假如有两个以上的正则表达式被同时匹配,长度较长的匹配胜出。

    3. 默认匹配*总是在所有的匹配都失败以后才会被激活。

  • 必须有一个session store能够用来存放session model。

    • 你可以用<match name="*">来匹配session model;

    • 也可以用精确匹配:<match name="SESSION_MODEL" />。其中session model的名字是必须和前述modelKey配置的值相同,其默认值为“SESSION_MODEL”。

8.2.5. Session Model

Session Model是用来记录当前session的生命期数据的,例如:session的创建时间、最近更新时间等。默认情况下,

  • 当需要保存session数据时,SessionModel对象将被转换成一个JSON字符串(如下所示),然后这个字符串将被保存在某个session store中:

    {id:"SESSION_ID",ct:创建时间,ac:最近访问时间,mx:最长不活动时间}
  • 需要读取时,先从store中读到上述格式的字符串数据,然后再把它解码成真正的SessionModel对象。

以上转换过程是通过一个SessionModelEncoder接口来实现的。为了提供更好的移植性,Session框架可同时支持多个SessionModelEncoder的实现。配置如下:

例 8.6. Session Model编码器的配置

<session>
    <session-model-encoders>
        <model-encoders:default-session-model-encoder />
        <model-encoders:model-encoder class="..." />
        <model-encoders:model-encoder class="..." />
    </session-model-encoders>
</session>

在上面的例子中,提供了三个SessionModelEncoder的实现。第一个是默认的实现,第二、第三个是任意实现。

  • 当从store取得SessionModel对象时,框架将依次尝试所有的encoder,直到解码成功为止。

  • 当将SessionModel对象保存到store之前,框架将使用第一个encoder来编码对象。

当你从不同的SessionModel编码方案中移植的时候,上述多encoders共存的方案可以实现平滑的过渡。

8.2.6. Session Interceptor

Session Interceptor拦截器的作用是拦截特定的事件,甚至干预该事件的执行结果。目前有两种拦截器接口:

表 8.3. Session Interceptor拦截器接口

接口功能
SessionLifecycleListener

监听以下session生命期事件:

  • Session被创建

  • Session被访问

  • Session被作废

SessionAttributeInterceptor

拦截以下session读写事件:

  • onRead – 拦截session.getAttribute()方法,可以修改所读取的数据。

  • onWrite – 拦截session.setAttribute()方法,可以修改所写到store中的数据。

Session框架自身已经提供了两个有用的拦截器:

表 8.4. Session Interceptor的实现

名称说明
<lifecycle-logger>

监听session生命期事件,并记录日志。

<attribute-whitelist>

控制session中的attributes,只允许白名单中所定义的attribute名称和类型被写入到或读出于session store中。

这个功能对于cookie store是很有用的。因为cookie有长度的限制,所以需要用白名单来限制写入到cookie中的数据数量和类型。

你可以同时配置多种拦截器,如下所示。

例 8.7. 配置session interceptors

<session>
  <request-contexts:interceptors
        xmlns="http://www.alibaba.com/schema/services/request-contexts/session/interceptors">

    <lifecycle-logger />

    <attribute-whitelist>
      <attribute name="_csrf_token" />
      <attribute name="_lang" />
      <attribute name="loginUser" type="com.alibaba...MyUser" />
      <attribute name="shoppingCart" type="com.alibaba....ShoppingCart" />
    </attribute-whitelist>

    <interceptor class="..." />

  </request-contexts:interceptors>
</session>

8.3. Cookie Store

Cookie Store的作用,是将session对象保存在客户端cookie中。Cookie Store减轻了服务器维护session数据的压力,从而提高了应用的扩展性和可用性。

另一方面,在现实应用中,很多地方都会直接读写cookie。读写cookie是一件麻烦的事,因为你必须要设置很多参数:domainpathhttpOnly...等很多参数。而操作HttpSession是一件相对简单的事。因此,webx主张把一切对cookie的读写,都转换成对session的读写

8.3.1. 多值Cookie Store

8.3.1.1. 最简配置

例 8.8. 最基本的cookie配置(/WEB-INF/webx.xml

<services:request-contexts xmlns="http://www.alibaba.com/schema/services/request-contexts">
    <buffered /> 
    <lazy-commit /> 
    ...
    <session>
        <stores>
            <session-stores:cookie-store id="temporaryClientStore">
                <session-stores:cookie name="tmp" />
            </session-stores:cookie-store>
        </stores>
        <store-mappings>
            <match name="*" store="temporaryClientStore" />
        </store-mappings>
    </session>
</services:request-contexts>

上面的配置创建了一个“临时”cookie(即随着浏览器关闭而清除),来作为默认的session对象的存储。

Cookie Store依赖其它两个Request Contexts: <buffered> <lazy-commit>。没有它们,就不能实现基于cookie的session。为什么呢?这要从HTTP协议谈起。下面是一个标准的HTTP响应的文本。无论你的服务器使用了何种平台(Apache HTTPD Server、Java Servlet/JSP、Microsoft IIS,……),只要你通过浏览器来访问,必须返回类似下面的HTTP响应:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=AywiPrQKPEzfF9OZ; Path=/
Content-Type: text/html;charset=GBK
Content-Language: zh-CN
Content-Length: 48
Date: Mon, 06 Nov 2006 07:59:38 GMT


<html>
<body>
……

我们注意到,HTTP响应分为Header和Content两部分。从“HTTP/1.1 200 OK”开始,到“<html>”之前,都是HTTP Header,后面则为HTTP Content。而cookie是在header中指定的。一但应用服务器开始向浏览器输出content,那就再也没有机会修改header了。问题就出在这里。作为session的cookie可以在应用程序的任何时间被修改,甚至可能在content开始输出之后被修改。但是此后修改的session将不能被保存到cookie中。

Java Servlet API的术语称“应用服务器开始输出content”为“response被提交”。你可以通过response.isCommitted()方法来判断这一点。那么,哪些操作会导致response被提交呢?

  • response.getWriter()getOutputStream()所返回的流中输出,累计达到服务器所设定的一个chunk的大小,通常为8K

  • 用户程序或系统调用response.flushBuffer()

  • 用户程序或系统调用response.sendError()转到错误页面。

  • 用户程序或系统调用response.sendRedirect()重定向。

只要避免上述情形的出现,就可以确保cookie可以被随时写入。前两个Request Contexts —— <buffered><lazy-commit>正好解决了上面的问题。第一个<buffered>将所有的输出到response.getWriter()getOutputStream()的内容缓存在内存里,直到最后一刻才真正输出到浏览器;第二个<lazy-commit>拦截了response对象中引起提交的方法,将它们延迟到最后才执行。这样就保证了在cookie被完整写入之前,response绝不会被任何因素提交。

此外,<buffered>不是专为session框架而设计的。Webx的页面布局系统也依赖于这个Request Context。

8.3.1.2. Cookie的参数

例 8.9. Cookie的参数

<session-stores:cookie-store id="temporaryClientStore"
        maxLength="3896" maxCount="5" checksum="false">

    <session-stores:cookie name="tmp" domain="" path="/" maxAge="0" httpOnly="true"
        secure="false" survivesInInvalidating="false" />

</session-stores:cookie-store>

上例中列出了所有关于cookie的参数,解释如下:

表 8.5. Cookie的参数

参数名称说明
name

指定cookie的名称。

假设名称为“tmp”,那么将生成tmp0tmp1tmp2等cookie。

多个cookie stores的cookie名称不能重复。

domain

指定cookie的域名。

这几个参数的默认值,均和Session ID cookie的设置相同。因此,一般不需要特别设置它们

path

指定cookie的路径。

maxAge

指定cookie的过期时间,单位是秒。

如果值为0,意味着cookie持续到浏览器被关闭(或称临时cookie)。

有效值必须大于0,否则均被认为是临时cookie。

httpOnly

在cookie上设置HttpOnly标记。

在IE6及更新版本中,可以缓解XSS攻击的危险。

secure

在cookie上设置Secure标记。

这样,只有在https请求中才可访问该cookie。

survivesInInvalidating

这是一个特殊的设置。如果它被设置成true,那么当session被作废(invalidate)时,这个cookie store中的对象会幸存下来,并带入下一个新的session中。

如果这个值为true,必须同时设置一个大于0maxAge

这个设置有什么用呢?比如,我们希望在cookie中记录最近登录的用户名,以方便用户再次登录。可以把这个用户名记录在一个cookie store中,并设置survivesInInvalidating=true。即使用户退出登录,或当前session过期,新的session仍然可以读到这个store中所保存的对象。

maxLength

指定每个cookie的最大长度。默认为3896,约3.8K

Cookie store会把所有对象序列化到cookie中。但是cookie的长度是不能超过4K的。如果cookie的长度超过这个设定,就把数据分发到新的cookie中去。因此每个cookie store实际可能产生好几个cookie。

假设cookie name为tmp,那么所生成的cookie的名称将分别为:tmp0tmp1tmp2,以此类推。

maxCount

指定cookie的最大个数。默认为5

因此,实际cookie store可生成的cookie总长度为:maxLength * maxCount。如果超过这个长度,cookie store将会在日志里面发出警告(WARN级别),并忽略store中的所有对象。

checksum

是否创建概要cookie。默认为false

有时由于域名、路径等设置的问题,会导致cookie紊乱。例如:发现同名的cookie、cookie缺失等错误。这些问题很难跟踪。概要cookie就是为检查这类问题提供一个线索。如果把这个开关打开,将会产生一个概要性的cookie。假如cookie name为tmp,那么概要cookie的名字将是tmpsum。概要cookie会指出当前store共有几个cookie,每个cookie的前缀等内容。当cookie的总数和内容与概要cookie不符时,系统将会在日志中提出详细的警告信息(DEBUG级别)。

请尽量不要在生产系统中使用这个功能。

8.3.1.3. Session Encoders

Session里保存的是Java对象,而cookie中只能保存字符串。如何把Java对象转换成合法的cookie字符串(或者将字符串恢复成对象)呢?这就是Session Encoder所要完成的任务。

例 8.10. 配置Session Encoders

<session-stores:cookie-store>
    ...
    <session-stores:encoders>
        <session-encoders:encoder class="..." />
        <session-encoders:encoder class="..." />
        <session-encoders:encoder class="..." />
    </session-stores:encoders>
</session-stores:cookie-store>

SessionModelEncoder类似,session框架也支持多个session encoders同时存在。

  • 保存session数据时,session框架将使用第一个encoder来将对象转换成cookie可接受的字符串;

  • 读取session数据时,session框架将依次尝试所有的encoders,直到解码成功为止。

这种编码、解码方案可让使用不同session encoders的系统之间共享cookie数据,也有利于平滑迁移系统。

Session框架提供了一种encoder的实现,编码的基本过程为:序列化、加密(可选)、压缩、Base64编码、URL encoding编码。

例 8.11. 配置Session Encoders的几种方案

  • 基本配置:用hessian算法(默认)来序列化,不加密。

    <session-stores:cookie-store>
        <session-stores:encoders>
            <session-encoders:serialization-encoder /> 
        </session-stores:encoders>
    </session-stores:cookie-store>

    这是默认实现。

  • 用aes算法加密。AES算法可支持128192256位的密钥,默认为keySize=128

    <session-stores:cookie-store>
        <session-stores:encoders>
            <session-encoders:serialization-encoder>
                <session-serializers:hessian-serializer /> 
                <session-encrypters:aes-encrypter key="0123456789abcdef" /> 
            </session-encoders:serialization-encoder>
        </session-stores:encoders>
    </session-stores:cookie-store>

    也可以明确指定hession序列化。

    添加AES加密算法,并提供密钥。

  • 改用java原生的序列化算法。使用hessian算法(默认)可大幅缩短序列化的长度,但使用java原生的序列化算法,具有最好的可移植性。

    <session-stores:cookie-store>
        <session-stores:encoders>
            <session-encoders:serialization-encoder>
                <session-serializers:java-serializer /> 
            </session-encoders:serialization-encoder>
        </session-stores:encoders>
    </session-stores:cookie-store>

    指定java序列化。

8.3.2. 单值Cookie Store

前面所描述的cookie store,是在一组cookie(如tmp0, tmp1, ...)中保存一组attributes的名称和对象。它所创建的cookie值,只有session框架自己才能解读。

假如有一些非webx的代码想要共享保存在cookie中的session数据,例如,Java Script代码、其它未使用webx框架的应用,希望能读取session数据,应该怎么办呢?Session框架提供了一种相对简单的“单值cookie store”可用来解决这个问题。顾名思义,单值cookie store就是在一个cookie中仅保存一个值或对象。

8.3.2.1. 最简配置

例 8.12. 单值cookie store基本配置

<session>
    <stores>
        ...
        <stores:single-valued-cookie-store id="loginNameCookie"> 
            <stores:cookie name="login" /> 
        </stores:single-valued-cookie-store>
    </stores>
    <store-mappings>
        ...
        <match name="loginName" store="loginNameCookie" /> 
    </store-mappings>
</session>

单值cookie store的ID是loginNameCookie

Cookie的名称是login

Session attribute的名称是loginName,attribute名称和cookie名称不必相同。

根据上面的配置,下面程序会生成cookie:login=myname

例 8.13. 访问单值cookie的代码

session.setAttribute("loginName", "myname");

需要注意的是,上述最简配置,只能用来存取字符串值。如果需要存取其它类型的对象,则需要配置Session Value Encoder。详见第 8.3.2.3 节 “Session Value Encoders”

8.3.2.2. Cookie的参数

例 8.14. 单值cookie的参数配置

<session-stores:single-valued-cookie-store id="loginNameCookie">

    <session-stores:cookie name="login" domain="" path="/" maxAge="0" httpOnly="true"
        secure="false" survivesInInvalidating="false" />

</session-stores:single-valued-cookie-store>

单值cookie的参数设置完全类似于普通cookie store的设置。唯一的差别是,单值cookie只生成一个cookie,而普通的cookie store则可能生成多个相关的cookies。

表 8.6. Cookie的参数

参数名称说明
name

指定cookie的名称。

domain

指定cookie的域名。

这几个参数的默认值,均和Session ID cookie的设置相同。因此,一般不需要特别设置它们

path

指定cookie的路径。

maxAge

指定cookie的过期时间,单位是秒。

如果值为0,意味着cookie持续到浏览器被关闭(或称临时cookie)。

有效值必须大于0,否则均被认为是临时cookie。

httpOnly

在cookie上设置HttpOnly标记。

在IE6及更新版本中,可以缓解XSS攻击的危险。

secure

在cookie上设置Secure标记。

这样,只有在https请求中才可访问该cookie。

survivesInInvalidating

这是一个特殊的设置。如果它被设置成true,那么当session被作废(invalidate)时,这个cookie store中的对象会幸存下来,并带入下一个新的session中。

如果这个值为true,必须同时设置一个大于0maxAge

这个设置有什么用呢?比如,我们希望在cookie中记录最近登录的用户名,以方便用户再次登录。可以把这个用户名记录在一个cookie store中,并设置survivesInInvalidating=true。即使用户退出登录,或当前session过期,新的session仍然可以读到这个store中所保存的对象。

8.3.2.3. Session Value Encoders

单值cookie store可以保存任意的Java对象,只要这个Java对象能够被转换成字符串,以及从字符串中恢复。将Java对象转换成字符串,以及从字符串中恢复,就是Session Value Encoder的任务。和前面所说的Session Encoder不同,Session Value Encoder只转换session attribute的值,而Session Encoder需要转换一组session attributes的key-values。

例 8.15. Session Value Encoders的配置

<session-stores:single-valued-cookie-store>
    ...
    <session-stores:encoders>
        <session-value-encoders:encoder class="..." />
        <session-value-encoders:encoder class="..." />
        <session-value-encoders:encoder class="..." />
    </session-stores:encoders>
</session-stores:single-valued-cookie-store>

SessionModelEncoder以及SessionEncoder类似,session框架也支持多个session value encoders同时存在。

  • 保存session数据时,session框架将使用第一个encoder来将对象转换成cookie可接受的字符串;

  • 读取session数据时,session框架将依次尝试所有的encoders,直到解码成功为止。

这种编码、解码方案可让使用不同session value encoders的系统之间共享cookie数据,也有利于平滑迁移系统。

目前有两种基本的session value encoders实现。<simple-value-encoder><mapped-values-encoder>。下面举例说明。

例 8.16. 配置Session Value Encoders的几种方案

  • 编码字符串值,以指定的charset对字符串进行URL encoding。

    <session-stores:encoders>
        <session-value-encoders:simple-value-encoder charset="GBK" /> 
    </session-stores:encoders>

    如不指定charset参数,默认charset为“UTF-8”。

  • 编码指定类型的值,该值具有默认的PropertyEditor,可以转换成String,或从String中恢复。

    <session-stores:encoders>
        <session-value-encoders:simple-value-encoder type="com.alibaba...MyEnum" /> 
    </session-stores:encoders>

    Spring直接支持将Enum类型的值转成String类型,或反之。

  • 编码指定类型的值,注册相应的registrar来进行类型转换。

    <session-stores:encoders>
        <session-value-encoders:simple-value-encoder type="java.util.Date">
            <session-value-encoders:property-editor-registrar
                class="com.alibaba.citrus.service.configuration.support.CustomDateRegistrar"
                p:timeZone="GMT+8" p:format="yyyy-MM-dd" /> 
        </session-value-encoders:simple-value-encoder>
    </session-stores:encoders>

    注册registrar,将Date类型按格式yyyy-MM-dd转成字符串,或从该格式的字符串中恢复。

  • 在上面例子的基础上,可增加encrypter,对value进行加密。

    <session-stores:encoders>
        <session-value-encoders:simple-value-encoder type="java.util.Date">
            <session-value-encoders:property-editor-registrar
                class="com.alibaba.citrus.service.configuration.support.CustomDateRegistrar"
                p:timeZone="GMT+8" p:format="yyyy-MM-dd" />
    
            <session-encrypters:aes-encrypter key="0123456789abcdef" /> 
    
        </session-value-encoders:simple-value-encoder>
    </session-stores:encoders>

    用AES和指定密钥进行加密。

  • <mapped-values-encoder><simple-value-encoder>类似,差别在于,前者只接受java.util.Map数据类型,并将其编码成“key:value&key:value”的格式。下面的例子可接受Map<String, Date>类型的数据:

    <session-stores:encoders>
        <session-value-encoders:mapped-values-encoder valueType="com.alibaba...MyEnum" /> 
    </session-stores:encoders>

    注意此处所指定的类型为map中的value的类型。

    当你用下面的代码,可设置cookie值“key1:value1&key2:value2”:

    Map<String, MyEnum> mappedValue = new HashMap<String, MyEnum>();
    
    mappedValue.put("key1", MyEnum.value1);
    mappedValue.put("key2", MyEnum.value2);
    
    session.setAttribute("cookie", mappedValue); 

    将整个map作为session attribute的值,其中,map的value类型必须符合配置文件中指定的类型。

  • 类似的,你同样可以对<mapped-values-encoder>指定registrar和encrypter,不再赘述。

8.4. 其它Session Store

8.4.1. Simple Memory Store

SimpleMemoryStore是最简单的session store。它将所有的session对象都保存在内存里面。这种store不支持多台机器的session同步,而且也不关心内存是否被用尽。因此这种简单的store一般只应使用于测试环境。

例 8.17. 配置simple memory store

<stores>
    <session-stores:simple-memory-store id="simple" />
</stores>
[警告]警告

鉴于simple-memory-store的实现的简单性,请不要将它应用在生产环境

8.5. 本章总结

Session是个难题,特别是对于要求高扩展性和高可用性的网站来说。

我们在标准的Java Servlet API的基础之上,实现了一套全新的session框架。在此基础上可以进一步实现多种session的技术,例如:基于cookie的session、基于数据库的session、基于Berkeley DB的session、基于内存的session,甚至也可以实现基于TCP-ring的session等等。最重要的是,我们能把这些技术结合起来,使每种技术的优点能够互补,缺点可以被避免。

所有这一切,对应用程序是完全透明的 —— 应用程序不用知道session是如何实现的、它们的对象被保存到哪个session store中等问题 —— session框架可以妥善地处理好这一切。