Session和Cookie原理
2017年02月28日

 

一、Session术语、通常意义上的概念

 

Session的英文含义为:会议,或者代表进行某活动、会议连续的一段时间(会期、学期)。

 

在计算机里面,通常是指某个连接或者操作的连续时间。Session的含义不仅仅局限于常见的HttpSession、浏览器会话,Session是一个广义的概念,HttpSession只是session的一种代表,比如对象的生命周期其实也可以看做是一个session,一个线程的完整运行也可以说是一个session周期。

 

二、HttpSession原理和基本作用

 

首先,HTTP协议本身是无状态的,每一次请求之间都是独立的,例如,客户端向服务器请求下载某些文件,下载完就结束这次服务了,服务器不一定非要纪录下客户的行为,下次客户再来下载,还是和上次一样操作就行了。

 

注意到一点,如果这个客户是个老顾客,他要连续的操作一段时间,而如果每次操作都要识别客户的身份,那未免也太麻烦了一点,所以JavaServlet中引入了HttpSession的概念,客户第一次来就记录下客户的身份,后面如果连续操作就直接使用第一次记录下来的信息,如果客户长时间不操作,这个记录下的身份信息就不再保存了,失效之后如果客户再来操作,又需要识别和记录客户身份。

 

再回过头来,由于HTTP协议本身是无状态的,但是很多情况下,我们又需要记录下一段连续时间内客户端的信息。其实,有三种方法,举例说明:张三曾经常去的一家咖啡店,有喝5杯咖啡免费赠1杯咖啡的优惠,然而一次性消费5杯咖啡的机会微乎其微,这时就需要某种方式来纪录某位顾客的消费数量。想象一下其实也无外乎下面的几种方案:

1、该店的店员很厉害,能记住每位顾客的消费数量,只要顾客一走进咖啡店,店员就知道该怎么对待了。这种做法就是协议本身支持状态

2、发给顾客一张卡片,上面记录着消费的数量,活动有一个截止日期,所以这个卡有一个有效期,过期就用不了了。每次消费时,如果顾客出示这张卡片,则此次消费就会与以前的消费相联系起来。这种做法就是在客户端保持状态

3、发给顾客一张会员卡,除了卡号之外什么信息也不纪录,或者客户自报姓名,每次消费时,如果顾客出示该卡片或者说出自己名字,则店员在店里的纪录本上找到这个卡号对应的纪录添加消费信息。这种做法就是在服务器端保持状态

 

这三种方案,都有各自的优劣,第一种,貌似很好,客户和商店都不需要记录客户的信息,但是需要店员主动去识别客户,好比在使用某协议时,就能够主动携带客户信息。但如果服务器不需要客户信息,那么协议里面就没必要携带客户信息了,所以HTTP协议没有要求,在协议里面必须包含客户信息,包括IP、客户端类型等,HTTP协议里面都不需要,但是HTTP协议支持以headerparam等形式传输这些客户端信息,使用完全自由。

 

第二种方案,优点在于,客户信息由客户自己提供、自己记录下来,服务端不需要知道、更不需要保存客户的信息,减少了服务端的工作。缺点是,客户信息由客户自己提供,服务端没有记录下客户的过往信息,那么客户可以对自己的信息作假,明明客户只消费了2杯咖啡,客户说他消费了20杯咖啡,那样肯定不行。还不如说银行卡密码等信息,肯定是要在服务端保存才行。

 

所以说,大多数时候,方案三才是我们需要的,服务端主动记录下客户信息,好处多多。

 

HttpSession的好处在于,它是一个服务端的标准的解决方案,它提供了一套标准的接口,自带了保存客户信息的容器(其实就是利用JavaMap把数据保存在内存里面)。

 

 

三、Cookie原理和基本作用

 

接着上面讲,HttpSession是服务端的标准解决方案,而Cookie就是客户端的标准解决方案(由W3C提出),就是上面例子中的第二种情况,客户自己保存自己的信息在一个叫做Cookie的容器里面。几乎所有的浏览器都支持Cookie,它不但可以保存在内存中,甚至可以持久化保存在磁盘上。通常Cookie要设置一个过期时间,过期之后保存的信息就被清除了,客户也可以自己随时手工清除。

 

 

四、Cookie具体实现和常用功能

 

上面只说了Cookie的基本原理和功能,现在来看Cookie的具体功能。首先,Cookie可以由浏览器自动生成和管理,当然也可以自己写客户端脚本JavaScriptVBScript等自己管理。

 

什么时候生成Cookie呢,正统的Cookie分发是通过扩展HTTP协议来实现的,服务器通过HTTP的响应头[header]中加上一行特殊的指示(Set-Cookie)以提示浏览器按照指示生成相应的cookie

 

Java中把Cookie封装成了javax.servlet.http.Cookie类。服务器通过操作Cookie类对象对客户端Cookie进行操作。通过Cookie[] request.getCookie()获取客户端提交的所有Cookie,通过response.addCookie(Cookie cookie)向客户端设置Cookie

 

Cookie怎么发送给服务器端呢?是由浏览器按照一定的原则在后台自动发送给服务器的(以请求头[header]的形式)。最重要的原则是:Cookie具有不可跨域名性

 

根据Cookie规范,浏览器访问Google只会携带GoogleCookie,而不会携带BaiduCookieGoogle也只能操作GoogleCookie,而不能操作BaiduCookie

Cookie在客户端是由浏览器来管理的。浏览器能够保证Google只会操作GoogleCookie而不会操作BaiduCookie,从而保证用户的隐私安全。浏览器判断一个网站是否能操作另一个网站Cookie的依据是域名。GoogleBaidu的域名不一样,因此Google不能操作BaiduCookie

 

cookie的内容主要包括:名字,值,过期时间(exxpires),路径(path)和域(domain

 

其中域可以指定某一个域比如.google.com,相当于总店招牌,也可以指定一个域下的具体某个子域名,比如www.google.com或者images.google.com。虽然网站images.google.com与网站www.google.com同属于Google,但是域名不一样,二者同样不能互相操作彼此的Cookie

 

路径就是跟在域名后面的URL路径,比如/或者/foo等等,路径与域合在一起就构成了cookie的作用范围。

 

如果不设置过期时间,则表示这个cookie的生命期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览器会话期的cookie被称为会话cookie。会话cookie一般不存储在硬盘上而是保存在内存里,当然这种行为并不是规范规定的。如果设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie仍然有效直到超过设定的过期时间。

 

存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存里的cookie,不同的浏览器有不同的处理方式。对于IE,在一个打开的窗口上按Ctrl-N(或者从文件菜单)打开的窗口可以与原窗口共享,而使用其他方式新开的IE进程则不能共享已经打开的窗口的内存cookie;对于Mozilla Firefox0.8,所有的进程和标签页都可以共享同样的cookie一般来说是用javascriptwindow.open打开的窗口会与原窗口共享内存cookie。浏览器对于会话cookie的这种只认cookie不认人的处理方式经常给采用session机制的web应用程序开发者造成很大的困扰。

 

下面就是一个goolge设置cookie的响应头的例子

HTTP/1.1 302 Found

Location: http://www.google.com/intl/zh-CN/

Set-Cookie: PREF=ID=0565f77e132de138:NW=1:TM=1098082649:LM=1098082649:S=KaeaCFPo49RiA_d8; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com

Content-Type: text/html

浏览器在再次访问goolge的资源时自动向外发送Cookie

 

另外,注意两点:

cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,可以考虑加密存储。

单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20cookie

 

五、Http Session的具体实现和常用功能

 

Http Session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息。

 

当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否已包含了一个session标识 - 称为session idServlet源码里面是调用这个方法:parseSessionCookiesId(request),如果已包含一个session id则说明以前已经为此客户端创建过session,服务器就按照session id把这个session检索出来使用(如果检索不到,可能会新建一个),如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session idsession id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个session id将被在本次响应中返回给客户端保存 保存这个session id的方式可以采用cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器。一般这个cookie的名字都是类似于SEEESIONID,但和后端服务器有关,比如weblogic对于web应用程序生成的cookie,它的名字就是JSESSIONID:(前面多一个J

JSESSIONID=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764

 

由于cookie可以被人为的禁止,必须有其他机制以便在cookie被禁止时仍然能够把session id传递回服务器。经常被使用的一种技术叫做URL重写就是把session id直接附加在URL路径的后面附加方式也有两种,一种是作为URL路径的附加信息,表现形式为

http://...../xxx;jsessionid=ByOK ... 99zWpBng!-145788764

另一种是作为查询字符串附加在URL后面,表现形式为

http://...../xxx?jsessionid=ByOK ... 99zWpBng!-145788764

这两种方式对于用户来说是没有区别的,只是服务器在解析的时候处理的方式不同,采用第一种方式也有利于把session id的信息和正常程序参数区分开来。

 

为了在整个交互过程中始终保持状态,就必须在每个客户端可能请求的路径后面都包含这个session id

另一种技术叫做表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session id传递回服务器。这种技术现在已较少应用,笔者接触过的很古老的iPlanet6(SunONE应用服务器的前身)就使用了这种技术。 实际上这种技术可以简单的用对action应用URL重写来代替。

 

Java Servlet提供的URL重新方案:

HttpServletResponse接口定义了两个用于URL重写的方法:

encodeURL方法,用于超链接和form表单的action属性中设置的URL进行重写

encodeRedirectURL方法,用于HttpServletResponse.sendRedirect方法的URL进行重写

 

例如:

<td>

    <a href="<%=response.encodeURL("index.jsp?c=1&wd=Java") %>">Homepage</a>

</td>

该方法会自动判断客户端是否支持Cookie。如果客户端支持Cookie,会将URL原封不动地输出来。如果客户端不支持Cookie,则会将用户Sessionid重写到URL中。重写后的输出可能是这样的:

<td>

    <a href="index.jsp;jsessionid=0CCD096E7F8D97B8AFDC3E1931E?c=1&wd=Java">

Homepage</a>

</td>

即在文件名的后面,在URL参数的前面添加了字符串“;jsessionid=XXX”。

 

查看HttpServletResponse.encodeURL(url)方法的源码,可以看到:

String tok = ";" +

            SessionConfig.getSessionUriParamName(request.getContext()) +

                "=" + session.getIdInternal();

其中,jsessionid这个名字也是可以配置的。

 

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

 

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

 

 

总结

session机制本身并不复杂,然而其实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器,服务器的经验当作普遍适用的经验,而是始终需要具体情况具体分析。

 

 

六、Http Session的持久化

 

持久化Http Session的作用,例如,在一个web应用程序重启时,服务器也会持久化该应用程序中所有HttpSession对象,保证客户端的会话活动仍可以恢复、继续。

 

Tomcat使用Session Manager类来管理Session的持久化,它提供了两个SessionManager

org.apache.catalina.session.StandardManager

org.apache.catalina.session.PersistentManager

 

StandardManagertomcat默认使用的,在web应用程序关闭时,对内存中的所有HttpSession对象进行持久化,把他们保存到文件系统中。默认的存储文件为

<tomcat 安装目录>/work/Catalina/<主机名>/<应用程序名>/sessions.ser

这个默认就是配置了的,如果要禁用,可以修改context.xml(全局配置)或者server.xml(单独配置):

<!-- Uncomment this to disable session persistence across Tomcat restarts -->

    <!--

    <Manager pathname="" />

    -->

当然,如果你是直接kill -9 tomcat进程,那session是没法保存的。

 

要实现更健壮、更符合生产环境的重启持久化,最好使用PersistentManager,它比StandardManager更为灵活,只要某个设备提供了实现org.apache.catalina.Store接口的驱动类,PersistentManager就可以将HttpSession对象保存到该设备。Tomcat自带提供了FileStoreJDBCStore两种实现,用户可以配置。

 

注意,默认情况下, session持久化,需要session里面的类实现java序列化接口java.io.Serializable,将对象序列化后保存。

 

 

七、扩展知识:实现集群环境下高可用Session

 

为了支持海量用户的访问,应用服务器集群这种水平扩展的方式是最常用的。在单机环境中,Session的创建和存储都是由同一个应用服务器实例来完成,而存储也仅是内存中,最多会在正常的停止服务器的时候,把当前活动的Session钝化到本地,再次启动时重新加载。

 

而多个实例之间,Session数据是完全隔离的。而为了实现Session的高可用,多实例间session数据共享是必然的。具体来说,常用有以下几种方案:

Ø  会话保持(案例:NginxHaproxy

Ø  会话复制(案例:TomcatWAS

Ø  会话共享(案例:MemcachedRedis

 

具体参见专题:《实现集群环境下的高可用Session》。