HTTP Cookie 与 Session 乱云

JSP、Java Servlet。不爽,我不爽东学点西学点,样样精通者样样稀松,怎么办?于是想通过 JSP、Servlet 搞明白其中的作用原理,HTTP 才是王道,和服务端语言有个辣子关系。Java 和我无关,但 Cookie、HTTP、Session 是放之四海的东西。Session 和 Cookie 更是论坛灌水、居家搭站必备良药。把玩下 JSP 是怎样把号称无状态的 HTTP 协议搞成有状态记录的,唯有此意。

之前,先说说网上的 Java 前辈们经常说道的几句金玉良言:
1. 了解 Cookie 和 Session 两种记录用户状态的方法。
2. 关掉浏览器后,会话就结束了。

下面是一个极度菜鸟的 C/C++ 程序员,在搞完 Session 后的一些谬论:
1. 什么 Session,哪有这玩意!
2. Cookie,当初怎么不叫 Handle?呵呵 HTTP Tracking Handle,或 fd?不,这个太俚了。
3. 关掉浏览器,会话就结束了?不一定吧?Shit!一定不!

从上面叙述风格中可以看出,那个菜鸟就是我。很讽刺的是,哪些金玉良言把我带入了困惑的深渊,而谬论多少让我懂得了一点东西。

Cookie
Cookie,参考:HTTP cookie @ Wikipedia。从介绍中可以知道一些关于 Cookie 的东西:

发端是一个由 Netscape 的 Lou Montulli 发明的浏览器操纵并管理的小东西,目的完全是应用驱动的,完成像购物车(virtual shopping cart)这种能记录用户信息的应用。

需要 Web 浏览器、服务器、HTTP 协议共同支持,Cookie 才能完成其作用。当然,当 IETF 还没有标准化 Cookie 技术的时候,Netscape 实际上在用它自家的 HTTP。

最后 IETF 有了几个和 Cookie 有关的标准:RFC 2109、RFC 2965,标题都叫 HTTP State Management Mechanism,后者是前者的修订版。不过似乎后来 Microsoft 和 Netscape 都不是很甩 IETF,所以 Netscape 和 IE 的 Cookie 具体实现嘛……,差不多就好。

Cookie 能做什么:Session managementPersonalizationTrackingThird-party cookies。看到了吧!Tracking!其实介绍中明确说了:cookie 也可以叫 tracking cookie。

Cookie 原理
看图:

HTTP、Cookie 标准或草案
1. 早期的 Netscape 的 Cookie 规格草案,Netscape 上的原地址已经废了,在这里有转帖:Persistent Client State – HTTP Cookies – Preliminary Specification

2. W3C 上关于 HTTP 的标准或草案,多数转到 IETF 上,不过这里的 RFC 都和 HTTP 有关,容易检索,当然也包括 HTTP 状态管理机制的规格:HTTP Specifications and Drafts

3. Mozilla、Firefox、Netscape 是同一血统,这是 Mozilla 上找到的关于 Cookies 的一点资料,算是正统吧:Appendix C Netscape Cookies

HttpSession 对象的实验
工具:HTTPLook,Firefox HttpFox 扩展

Servlet 中 session 是 HttpSession 类的对象,在 JSP 中直接可以使用。其实,我管你是什么对象呢,我只看 HTTP 包,Java 代码嘛,记住有个 session.getId()、session.setAttribute(“foo”, “bar”)、session.getAttribute(“foo”) 就行了,看看下面抓到的包:

1. 请求页面 session.jsp:

GET /Webzx/session.jsp HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.7,chrome://global/locale/intl.properties;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: GB2312,utf-8;q=0.7,*;q=0.7
Keep-Alive: 115
Connection: keep-alive

2. 页面 session.jsp 的响应消息:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=28A0EBF7EBB17CE16931FA7FD99EBAC3; Path=/Webzx
Content-Type: text/html;charset=UTF-8
Content-Length: 598
Date: Mon, 29 Mar 2010 10:07:48 GMT
 
[这里省略 HTML 报文]

3. session.jsp 上有个表单,action 为 tom.jsp,提交 method 为 post,点击提交后,转到 tom.jsp 页面,请求消息如下:

POST /Webzx/tom.jsp HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.7,chrome://global/locale/intl.properties;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: GB2312,utf-8;q=0.7,*;q=0.7
Keep-Alive: 115
Connection: keep-alive
Referer: http://localhost:8080/Webzx/session.jsp
Cookie: JSESSIONID=28A0EBF7EBB17CE16931FA7FD99EBAC3
Content-Type: application/x-www-form-urlencoded
Content-Length: 33
 
# 这是表单中的 text 和 sumbit 型的 input,提交后的 POST 内容。
# % 编码的是 sumbit 的 value,中文“提交”的 UTF-8 编码。
boy=abc&submit=%E6%8F%90%E4%BA%A4

看到这里,大概明白 Servlet 的 Session 是怎么实现的了。不玩小儿科了,记住几个关键词就行:响应消息 Set-Cookie 头域,请求消息 Cookie 头域,JSESSIONIDHttpSession.getId()

禁用 Cookie

很容易想到如果禁用 Cookie,Servlet 的效果会怎样。结果是上面的简单 Session 应用不起作用了,每跳转一个新的页面,Servlet 都会给一个新的 Session ID,换句话说服务端的 Servlet 已经不认识上一次和它连接的浏览器了。

禁用 Cookie 后的行为和浏览器的实现有关,Firefox 对 Cookie 禁用后,就是上面情况。此时,Firefox 在请求中就不会加 Cookie 头域,当然如果你不停的刷网页的话,服务器会每次义无反顾地响应 Set-Cookie 头域,里面带有每次都是新的 JSESSIONID,而 Firefox 也会每次义无反顾的忽略这个头域。

看来 Servlet 的 Session 是基于 Cookie 实现的,至少常规情况下是这样。Session 的 Cookie 和其它的 Cookie 的区别,完全在于用户怎么使用它,是直接存放用户的信息明细,还是只存一个 ID 号做跟踪用,实际的信息存放在服务端。

怎么解决这个问题?服务端的 Servlet 不认识上一次和它连接的浏览器了,只因为浏览器没把上次的 Session ID 保存起来,并在这次请求时带给服务端。制定 Servlet 标准的人早就想到了,当然有其它候选方法让会话特性不依赖于 Cookie 存在。摘录一段如下:

response.encodeURL() 的用法
关键字:URL 重写,response.encodeUrl(),response.encodeRedirectUrl()

Servlet API 中定义了 javax.servlet.http.HttpSession 接口,Servlet 容器必须实现这个接口。当一个 Session 开始时,Servlet 容器将创建一个 HttpSession 对象,Servlet 容器为 HttpSession 分配一个唯一标识符,称为 Session ID。Servlet 容器将 Session ID 作为 Cookie 保存在客户的浏览器中。每次客户发出 HTTP 请求时,Servlet 容器可以从 HttpRequest 对象中读取 Session ID,然后根据 Session ID 找到相应的 HttpSession 对象,从而获取客户的状态信息。

当客户端浏览器中禁止 Cookie,Servlet 容器无法从客户端浏览器中取得作为 Cookie 的 Session ID,也就无法跟踪客户状态。

Java Servlet API 中提出了跟踪 Session 的另一种机制,如果客户端浏览器不支持 Cookie,Servlet 容器可以重写客户请求的 URL,把 Session ID 添加到 URL 信息中。

HttpServletResponse 接口提供了重写 URL 的方法:public java.lang.String encodeURL(java.lang.String url)。

该方法的实现机制为:
1. 先判断当前的 Web 组件是否启用 Session,如果没有启用 Session,直接返回参数 url。
2. 再判断客户端浏览器是否支持 Cookie,如果支持 Cookie,直接返回参数 url;如果不支持 Cookie,就在参数 url 中加入 Session ID 信息,然后返回修改后的 url。

我们可以对网页中的链接稍作修改,解决以上问题:
修改前:

<a href="maillogin.jsp">

修改后:

<a href="<%=response.encodeURL("maillogin.jsp")%>">

看到了吗?这是什么?能总结出什么?
1. 不管怎样,只要把上次浏览器连接时,服务端给其分配的 Session ID 传给浏览器,而后浏览器下次跳链接的时候将此 ID 回传给服务端,Session 机制就能实现。我管你是怎么来回传的,传到服务端是靠框架找出对应的 Session 对象,还是手工编码去做。encodeURL()/encodeRedirectUrl() 的好处是,它靠 Servlet 框架就能找出 Session,不需要应用程序员去搞了,它的另一个好处就是 2。

2. Session 还是倾向于使用 Cookie 来实现,直到万不得已的时候,才会重写 URL。

encodeURL()/encodeRedirectUrl() 是怎么做的呢?替换你作为响应消息体返回的 HTML,把链接改造成 http://host/foo.jsp;jsessionid=[Session ID Value] 的形式,可以把鼠标移到链接上看看它的跳转地址,从跳转后的地址栏也能看出端倪。后端的 Servlet 工作就是根据请求消息第一行中的 Request-URI 中的 ;jsessionid=[Session ID Value] 找到它关联的 HttpSession 对象,上次是 Cookie 头域,这次是请求行的 Request-URI,仅此而已。

再扩展一下:
* 既然是 Request-URI,那么用 GET 请求的问号(?)加参数串也可以传 Session ID。
* 用 GET method 的表单的 hiden 型 input,就是产生上面的 GET 串,所以用它也可以传 Session ID。
* POST method 的表单 hiden input?也可以传。

我们只需要在服务端解析传来的 Session ID,并用它找到关联的 HttpSession 对象即可。

Session 过期
列两个名词:会话 cookie(session cookie),持久 cookie(persistent cookie)。

服务器响应头域中的 Set-Cookie,在传递 Servlet 的 Session ID 时,除了 JSESSIONID 值外(cookie 变量名),还可以传递其它几个 cookie 属性参数,Netscape Cookies 规范中说道:

Set-Cookie:
	name=value
	[;EXPIRES=dateValue]
	[;DOMAIN=domainName]
	[;PATH=pathName]
	[;SECURE]

其中,EXPIRES 的含义为:

EXPIRES=dateValue specifies a date string that defines the valid life time of that cookie. Once the expiration date has been reached, the cookie will no longer be stored or given out. If you do not specify dateValue, the cookie expires when the user's session ends.
 
The date string is formatted as:
 
Wdy, DD-Mon-YY HH:MM:SS GMT
 
where Wdy is the day of the week (for example, Mon or Tues); DD is a two-digit representation of the day of the month; Mon is a three-letter abbreviation for the month (for example, Jan or Feb); YY is the last two digits of the year; HH:MM:SS are hours, minutes, and seconds, respectively.

意思是如果没有提供这个字段,cookie 将会在 user’s session 结束后到期。但是什么是 user’s session ends,规范没给出具体含义,而且这里的叙述可能有循环定义的嫌疑:Servlet 的 HttpSession 依靠 cookie 工作,而这里的又说在 Session 结束后过期 cookie。一句话,此 Session 非彼 Session 也,一个是技术上的,一个是语义上的。也许 Session 并不存在,所有的需求都来自于对浏览器状态的跟踪,而采用的技巧方法。

持久 cookie:由 Servlet 的开发者自己创建、维护、使用的 cookie,如果你指定了 EXPIRES 属性,则 cookie 会存储在浏览器数据中(cookie 文件或是其它数据形式)——即使重启浏览器也还在,直到 EXPIRES 时间过期才废掉它。

会话 cookie:由某种服务端框架(像 Java Servlet),自动创建、使用的 cookie,目的是基于它去构造抽象层次更高的 Session 概念。如果此时不指定 EXPIRES 属性,则在浏览器达到某种状态时,就废掉它。而这种状态就是 cookie 规范中所说的 user’s session ends,视浏览器的具体实现而定,即使同一浏览器也视具体的操作行为而定,比如:关闭所有同一浏览器程序,关闭单个浏览器程序,关闭浏览器 Tab 页,关闭子窗口等。

我测试了下 Firefox 3.6 的 user’s session ends 判定,如下:

当打开多个 Tab 页浏览时,且不启用隐私模式时(在 选项→隐私 中设置,隐私模式不保存浏览历史),如果关闭总的 Firefox 程序,会提示“是否保存 Tab,并在下次启动打开它们?”,如果选择了“保存并退出”,则下次重启 Firefox 时,会话 cookie 仍在;如果选择了“退出”,这些浏览 Tab 就不保存,并且下次重启 Firefox 时,会话 cookie 就没了。

如果这还不能说明缺省 EXPIRES 的 cookie 过期和浏览器具体实现和行为有关的话,再看看这个:

Firefox 中有个 Session Manager 的扩展(一直在用它,很好用,现在才知道它的机制)。装上 Session Manager 后,先对基于“缺省 EXPIRES 的 cookie”会话浏览页面做一次“会话保存”,然后清除 Firefox 中所有的 cookie,关闭 Firefox,关闭电脑去喝杯茶,不过不要太长。

真正 Session 的释放与销毁是在服务端,如果浏览器处于非活动状态超过一定时间,Servlet 框架就会废掉这个 HttpSession 对象,与它关联的 Session ID 也就没有用了,这时即使浏览器把含有 JSESSIONID 的 Cookie 回传给服务端也没有用了,服务端已经找不见对应的 HttpSession 对象了。

这个非活动间隔时间可由 Servlet 开发者设置:

1.在 WEB-INF/web.xml 中设置全局的 session 非活动间隔时间:

<session-config>
<session-timeout>xxx</session-timeout>
</session-config>

在 Tomcat 的实现中,它的缺省值是 1800 秒(30 分钟)。

2.在 Servlet 中使用 HttpSession.setMaxInactiveInterval(int interval) 方法。

回到电脑前打开 Firefox,打开刚才带会话的页面,但是这时已经不认识你了,是一个新的会话(比如要求重新登录,购物车中为空等等),这是必须的,因为你把会话 cookie 清掉了。然后用 Session Manager 打开你刚保存的会话,Firefox 一顿晃动之后,你上回的 Session 又回来了(登录 ID,购物车中的东西又回来了)。想想就知道它是怎么做的:Session Manager 在 Firefox 的配置目录下创建一个 sessions 的子目录,里面保存着浏览的页面信息,当然也包括会话 cookie。

最后引用《Cookie 和 Session 的工作机制》中的一段话:

在谈论 session 机制的时候,常常听到这样一种误解:“只要关闭浏览器,session 就消失了”。其实可以想象一下会员卡的例子,除非顾客主动对店家提 出销卡,否则店家绝对不会轻易删除顾客的资料。对session来说也是一样的,除非程序通知服务器删除一个 session,否则服务器会一直保留,程序一般都是在用户做 log off 的时候发个指令去删除 session。然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分 session 机制都使用会话 cookie 来保存 session id,而关闭浏览器后这个 session id 就消失了,再次连接服务器时也就无法找到原来的 session。如果服务器设置的 cookie 被保存到硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 session id 发送给服务器,则再次打开浏览器仍然能够找到原来的 session。

恰恰是由于关闭浏览器不会导致 session 被删除,迫使服务器为 seesion 设置了一个失效时间,当距离客户端上一次使用 session 的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把 session 删除以节省存储空间。

From: http://zyxhome.org/wp/network/web/http-cookie-session-note/

One thought on “HTTP Cookie 与 Session 乱云”

Leave a Reply

Your email address will not be published. Required fields are marked *