一、术语session在我的经验里,session这个词被滥用的程度大概仅次于transaction,更加有趣的是transaction与session在某些语境下的含义是相同的。
二、HTTP协议与状态保持HTTP协议本身是无状态的,这与HTTP协议本来的目的是相符的,客户端只需要简单的向服务器请求下载某些文件,无论是客户端还是服务器都没有必要纪录彼此过去的行为,每一次请求之间都是独立的,好比一个顾客和一个自动售货机或者一个普通的(非会员制)大卖场之间的关系一样。
然而聪明(或者贪心?)的人们很快发现如果能够提供一些按需生成的动态信息会使web变得更加有用,就像给有线电视加上点播功能一样。这种需求一方面迫使HTML逐步添加了表单、脚本、DOM等客户端行为,另一方面在服务器端则出现了CGI规范以响应客户端的动态请求,作为传输载体的HTTP协议也添加了文件上载、cookie这些特性。其中cookie的作用就是为了解决HTTP协议无状态的缺陷所作出的努力。至于后来出现的session机制则是又一种在客户端与服务器之间保持状态的解决方案。
让我们用几个例子来描述一下cookie和session机制之间的区别与联系。笔者曾经常去的一家咖啡店有喝5杯咖啡免费赠一杯咖啡的优惠,然而一次性消费5杯咖啡的机会微乎其微,这时就需要某种方式来纪录某位顾客的消费数量。想象一下其实也无外乎下面的几种方案:1、该店的店员很厉害,能记住每位顾客的消费数量,只要顾客一走进咖啡店,店员就知道该怎么对待了。这种做法就是协议本身支持状态。2、发给顾客一张卡片,上面记录着消费的数量,一般还有个有效期限。每次消费时,如果顾客出示这张卡片,则此次消费就会与以前或以后的消费相联系起来。这种做法就是在客户端保持状态。3、发给顾客一张会员卡,除了卡号之外什么信息也不纪录,每次消费时,如果顾客出示该卡片,则店员在店里的纪录本上找到这个卡号对应的纪录添加一些消费信息。这种做法就是在服务器端保持状态。
由于HTTP协议是无状态的,而出于种种考虑也不希望使之成为有状态的,因此,后面两种方案就成为现实的选择。具体来说cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。同时我们也看到,由于采用服务器端保持状态的方案在客户端也需要保存一个标识,所以session机制可能需要借助于cookie机制来达到保存标识的目的,但实际上它还有其他选择。
正统的cookie分发是通过扩展HTTP协议来实现的,服务器通过在HTTP的响应头中加上一行特殊的指示以提示浏览器按照指示生成相应的cookie。然而纯粹的客户端脚本如JavaScript或者VBScript也可以生成cookie。
存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存里的cookie,不同的浏览器有不同的处理方式。对于IE,在一个打开的窗口上按Ctrl-N(或者从文件菜单)打开的窗口可以与原窗口共享,而使用其他方式新开的IE进程则不能共享已经打开的窗口的内存cookie;对于MozillaFirefox0.8,所有的进程和标签页都可以共享同样的cookie。一般来说是用javascript的window.open打开的窗口会与原窗口共享内存cookie。浏览器对于会话cookie的这种只认cookie不认人的处理方式经常给采用session机制的web应用程序开发者造成很大的困扰。
这是使用HTTPLook这个HTTPSniffer软件来俘获的HTTP通讯纪录的一部分
浏览器在再次访问goolge的资源时自动向外发送cookie
使用Firefox可以很容易的观察现有的cookie的值使用HTTPLook配合Firefox可以很容易的理解cookie的工作原理。
IE也可以设置在接受cookie前询问
这是一个询问接受cookie的对话框。
四、理解session机制session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。
保存这个sessionid的方式可以采用cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器。一般这个cookie的名字都是类似于SEEESIONID,而。比如weblogic对于web应用程序生成的cookie,JSESSIONID=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764,它的名字就是JSESSIONID。
另一种技术叫做表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把sessionid传递回服务器。比如下面的表单
一般情况下,session都是存储在内存里,当服务器进程被停止或者重启的时候,内存里的session也会被清空,如果设置了session的持久化特性,服务器就会把session保存到硬盘上,当服务器进程重新启动或这些信息将能够被再次使用,WeblogicServer支持的持久性方式包括文件、数据库、客户端cookie保存和复制。
复制严格说来不算持久化保存,因为session实际上还是保存在内存里,不过同样的信息被复制到各个cluster内的服务器进程中,这样即使某个服务器进程停止工作也仍然可以从其他进程中取得session。
cookie的路径对于web应用程序来说是一个非常重要的选项,WeblogicServer对这个选项的默认处理方式使得它与其他服务器有明显的区别。后面我们会专题讨论。
六、HttpSession常见问题(在本小节中session的含义为⑤和⑥的混合)
1、session在何时被创建一个常见的误解是以为session在有客户端访问时就被创建,然而事实是直到某server端程序调用HttpServletRequest.getSession(true)这样的语句时才被创建,注意如果JSP没有显示的使用<%@pagesession="false"%>关闭session,则JSP文件在编译成Servlet时将会自动加上这样一条语句HttpSessionsession=HttpServletRequest.getSession(true);这也是JSP中隐含的session对象的来历。
由于session会消耗内存资源,因此,如果不打算使用session,应该在所有的JSP中关闭它。
3、如何做到在浏览器关闭时删除session严格的讲,做不到这一点。可以做一点努力的办法是在所有的客户端页面里使用javascript代码window.oncolose来监视浏览器的关闭动作,然后向服务器发送一个请求来删除session。但是对于浏览器崩溃或者强行杀死进程这些非常规手段仍然无能为力。
4、有个HttpSessionListener是怎么回事你可以创建这样的listener去监控session的创建和销毁事件,使得在发生这样的事件时你可以做一些相应的工作。注意是session的创建和销毁动作触发listener,而不是相反。类似的与HttpSession有关的listener还有HttpSessionBindingListener,HttpSessionActivationListener和HttpSessionAttributeListener。
5、存放在session中的对象必须是可序列化的吗不是必需的。要求对象可序列化只是为了session能够在集群中被复制或者能够持久保存或者在必要时server能够暂时把session交换出内存。在WeblogicServer的session中放置一个不可序列化的对象在控制台上会收到一个警告。我所用过的某个iPlanet版本如果session中有不可序列化的对象,在session销毁时会有一个Exception,很奇怪。
7、开两个浏览器窗口访问应用程序会使用同一个session还是不同的session参见第三小节对cookie的讨论,对session来说是只认id不认人,因此不同的浏览器,不同的窗口打开方式以及不同的cookie存储方式都会对这个问题的答案有影响。
8、如何防止用户打开两个浏览器窗口操作导致的session混乱这个问题与防止表单多次提交是类似的,可以通过设置客户端的令牌来解决。就是在服务器每次生成一个不同的id返回给客户端,同时保存在session里,客户端提交表单时必须把这个id也返回服务器,程序首先比较返回的id与保存在session里的值是否一致,如果不一致则说明本次操作已经被提交过了。可以参看《J2EE核心模式》关于表示层模式的部分。需要注意的是对于使用javascriptwindow.open打开的窗口,一般不设置这个id,或者使用单独的id,以防主窗口无法操作,建议不要再window.open打开的窗口里做修改操作,这样就可以不用设置。
9、为什么在WeblogicServer中改变session的值后要重新调用一次session.setValue做这个动作主要是为了在集群环境中提示WeblogicServersession中的值发生了改变,需要向其他服务器进程复制新的session值。
10、为什么session不见了排除session正常失效的因素之外,服务器本身的可能性应该是微乎其微的,虽然笔者在iPlanet6SP1加若干补丁的Solaris版本上倒也遇到过;浏览器插件的可能性次之,笔者也遇到过3721插件造成的问题;理论上防火墙或者代理服务器在cookie处理上也有可能会出现问题。出现这一问题的大部分原因都是程序的错误,最常见的就是在一个应用程序中去访问另外一个应用程序。我们在下一节讨论这个问题。
七、跨应用程序的session共享
常常有这样的情况,一个大项目被分割成若干小项目开发,为了能够互不干扰,要求每个小项目作为一个单独的web应用程序开发,可是到了最后突然发现某几个小项目之间需要共享一些信息,或者想使用session来实现SSO(singlesignon),在session中保存login的用户信息,最自然的要求是应用程序间能够访问彼此的session。
然而按照Servlet规范,session的作用范围应该仅仅限于当前应用程序下,不同的应用程序之间是不能够互相访问对方的session的。各个应用服务器从实际效果上都遵守了这一规范,但是实现的细节却可能各有不同,因此解决跨应用程序session共享的方法也各不相同。
首先来看一下Tomcat是如何实现web应用程序之间session的隔离的,从Tomcat设置的cookie路径来看,它对不同的应用程序设置的cookie路径是不同的,这样不同的应用程序所用的sessionid是不同的,因此即使在同一个浏览器窗口里访问不同的应用程序,发送给服务器的sessionid也可以是不同的。
根据这个特性,我们可以推测Tomcat中session的内存结构大致如下。
笔者以前用过的iPlanet也采用的是同样的方式,估计SunONE与iPlanet之间不会有太大的差别。对于这种方式的服务器,解决的思路很简单,实际实行起来也不难。要么让所有的应用程序共享一个sessionid,要么让应用程序能够获得其他应用程序的sessionid。
iPlanet中有一种很简单的方法来实现共享一个sessionid,那就是把各个应用程序的cookie路径都设为/(实际上应该是/NASApp,对于应用程序来讲它的作用相当于根)。
需要注意的是,操作共享的session应该遵循一些编程约定,比如在sessionattribute名字的前面加上应用程序的前缀,使得setAttribute("name","neo")变成setAttribute("app1.name","neo"),以防止命名空间冲突,导致互相覆盖。
在Tomcat中则没有这么方便的选择。在Tomcat版本3上,我们还可以有一些手段来共享session。对于版本4以上的Tomcat,目前笔者尚未发现简单的办法。只能借助于第三方的力量,比如使用文件、数据库、JMS或者客户端cookie,URL参数或者隐藏字段等手段。
我们再看一下WeblogicServer是如何处理session的。
从截屏画面上可以看到WeblogicServer对所有的应用程序设置的cookie的路径都是/,这是不是意味着在WeblogicServer中默认的就可以共享session了呢?然而一个小实验即可证明即使不同的应用程序使用的是同一个session,各个应用程序仍然只能访问自己所设置的那些属性。这说明WeblogicServer中的session的内存结构可能如下
对于这样一种结构,在session机制本身上来解决session共享的问题应该是不可能的了。除了借助于第三方的力量,比如使用文件、数据库、JMS或者客户端cookie,URL参数或者隐藏字段等手段,还有一种较为方便的做法,就是把一个应用程序的session放到ServletContext中,这样另外一个应用程序就可以从ServletContext中取得前一个应用程序的引用。示例代码如下,
应用程序Acontext.setAttribute("appA",session);
应用程序BcontextA=context.getContext("/appA");HttpSessionsessionA=(HttpSession)contextA.getAttribute("appA");
值得注意的是这种用法不可移植,因为根据ServletContext的JavaDoc,应用服务器可以处于安全的原因对于context.getContext("/appA");返回空值,以上做法在WeblogicServer8.1中通过。
八、总结session机制本身并不复杂,然而其实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器,服务器的经验当作普遍适用的经验,而是始终需要具体情况具体分析。
5、webgarden模式下能使用InProc的Session吗?在WebGarden模式下,同一应用程序目录下ASP.NET的进程进程不只一个,同一个页面的请求可能会被不同的进程来处理,由于InProc模式的Session是保存在ASP.NET进程内的,这会导致Session紊乱。
////////////////////////////////////////////////////////////
HttpSessionState.IsCookieless属性获取一个值,该值指示会话ID是嵌入在URL中还是存储在HTTPCookie中。
命名空间:System.Web.SessionState程序集:System.Web(在system.web.dll中)
语法
VisualBasic(用法)DiminstanceAsHttpSessionStateDimvalueAsBoolean
value=instance.IsCookieless
C#publicboolIsCookieless{get;}
属性值如果会话嵌入在URL中,则为true;否则,为false。备注
ASP.NET可唯一标识每个浏览器使用的会话。默认条件下,会话的唯一标识符存储在浏览器的不过期会话Cookie中。通过在sessionState配置元素中将cookieless属性设置为true,可以指定不将会话标识符存储在Cookie中。
注意要提高应用程序的安全性,您的应用程序必须允许用户注销,以便此时它可以调用Abandon方法。这样可以减少不必要代码使用URL中唯一标识符检索存储在会话中的用户专用数据的潜在风险。
ASP.NET通过自动把一个唯一的会话ID插入页URL来保持无Cookie会话状态。例如,下面的URL已被ASP.NET修改,以包含唯一的会话IDlit3py55t21z5v55vlm25s55:
会话ID紧随在应用程序名后的斜杠之后,在其余所有文件或虚拟目录标识符之前。这样可使ASP.NET在请求中包含SessionStateModule之前解析出应用程序名。
默认条件下,无Cookie会话中使用的会话标识符将被回收。也就是说,如果使用已过期的会话ID发送了请求,则新会话将使用随请求一起提供的会话ID启动。当某个包含无Cookie会话ID的链接与多个浏览器共享时(可能通过搜索引擎或其他程序),这种行为可能会导致有害的会话数据共享。您可以通过禁用会话标识符的回收来减少会话数据被多个客户端共享的可能性。若要完成此操作,请将sessionState配置元素的regenerateExpiredSessionId属性设置为true。当使用已过期的会话ID发送无Cookie会话请求时,这种设置可以生成新的会话ID。请注意如果使用已过期会话ID发送的请求使用HTTPPOST方法,当regenerateExpiredSessionId为true时,任何已发送的数据都将丢失,因为ASP.NET需要执行重定向,以确保浏览器在URL中存在新会话标识符。
注意尽管将regenerateExpiredSessionId属性设置为true可以减少不必要的会话数据共享的可能性,但这种操作无法防止不必要代码获取SessionID值,并包含在发往服务器的请求中,从而获权访问另一用户的会话。如果您需要在会话状态中存储专用或敏感信息,建议您使用SSL对浏览器和包含SessionID的服务器之间的任何通信进行加密。
示例
下面的代码示例在Web.config文件中将cookieless会话属性设置为true。
复制代码
下面的代码示例演示如何在页上的文本框中显示浏览器信息。
注意HttpBrowserCapabilities对象所公开的属性指示浏览器的内在功能,但不一定反映出当前的浏览器设置。例如,Cookies属性指示浏览器是否内在地支持Cookie,但不指示发出请求的浏览器是否已启用了Cookie。有关更多信息,请参见ASP.NETCookie概述。
C#复制代码privatevoidButton1_Click(objectsender,System.EventArgse){System.Web.HttpBrowserCapabilitiesbrowser=Request.Browser;strings="BrowserCapabilities\n"+"Type="+browser.Type+"\n"+"Name="+browser.Browser+"\n"+"Version="+browser.Version+"\n"+"MajorVersion="+browser.MajorVersion+"\n"+"MinorVersion="+browser.MinorVersion+"\n"+"Platform="+browser.Platform+"\n"+"IsBeta="+browser.Beta+"\n"+"IsCrawler="+browser.Crawler+"\n"+"IsAOL="+browser.AOL+"\n"+"IsWin16="+browser.Win16+"\n"+"IsWin32="+browser.Win32+"\n"+"SupportsFrames="+browser.Frames+"\n"+"SupportsTables="+browser.Tables+"\n"+"SupportsCookies="+browser.Cookies+"\n"+"SupportsVBScript="+browser.VBScript+"\n"+"SupportsJavaScript="+browser.EcmaScriptVersion.ToString()+"\n"+"SupportsJavaApplets="+browser.JavaApplets+"\n"+"SupportsActiveXControls="+browser.ActiveXControls+"\n";TextBox1.Text=s;}
/////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////