掌握了会话状态基本知识之后,让我们通过分析会话状态管理的技术细节,增强我们的技能。会话状态处理是一个可以用以下三个步骤概括的任务:分配一个会话ID;从一个提供程序那里获取会话数据;把它填充到页面的上下文中。如前所述,会话状态模块控制所有这些任务的执行。这样做时,它利用两个额外组件:会话ID生成器和会话状态提供程序。在ASP.NET 2.0中,这两个组件都可以用定制组件代替,后文将对此进行介绍。现在,要解决使用会话状态时面临的实际问题。
13.3.1 标识一个会话
每个活动的ASP.NET会话使用一个只由URL允许的字符组成的120位字符串进行标识。会话ID被保证是惟一的并且是随机产生的,以避免数据冲突和防止恶意攻击。根据现有ID通过算法来获得一个有效的会话ID实际上并不可行。在ASP.NET 1.x中,会话ID生成器是一个埋藏在框架中的系统组件,对外部不可见。在ASP.NET 2.0中,会话ID生成器变成了一个可定制的组件,开发人员可以有选择地替换它。
注意
一个古老的谚语提醒了我们,任何事情都不能仅仅因为可行就去做(nothing should be done only because it is doable)。这个谚语特别适合这里的情形,因为我们在讨论ASP.NET 2.0中可定制的会话状态管理。这些子系统(诸如会话ID生成器)只有在理由充分的情况下才进行定制,并且确保这么做不会使事情变得更糟或者降低安全水平。稍后我将更详细地讨论这一点。
1. 创建会话ID
根据设计,会话ID长15字节(15×8=120位)。会话ID是使用随机数生成器(Random Number Generator,简称RNG)密码提供程序生成。该服务提供程序返回一个由15个随机生成的数值组成的数组。然后把该数组映射到有效的URL字符,并作为一个字符串返回。
如果会话没有包含任何数据,则为每个请求生成一个新的会话ID,并且会话状态并不持久地存储在状态提供程序中。然而,如果使用一个 Session_Start处理程序,则会话状态总是被保存,即使它为空。由于这个原因,特别是在没有使用进程内会话提供程序时,定义 Session_Start处理程序要特别小心,并且只有在确实需要时才定义。
相反,在一个非空的会话字典超时或被丢弃之后,会话ID仍然不变。根据设计,即使会话状态到期,会话ID一直延续到浏览器会话结束为止。这就是说,只要浏览器实例保持相同,则同一个会话ID用来表示一段时间内的多个会话。
2. 会话cookie
SessionID字符串传递给浏览器,然后以如下两种方法之一返回给服务器应用程序:使用一个cookie或一个修改过的URL。在默认情况下,会话状态模块在客户端创建一个HTTP cookie,但也可以使用一个修改过的URL,其中嵌入SessionID字符串,特别针对无cookie的浏览器。具体采取哪种方法取决于应用程序的 web.config文件中存储的配置设置。在默认情况下,会话状态使用cookie。
实际上,cookie只不过是网页放在客户端硬盘上的一个文本文件。在ASP.NET中,cookie由HttpCookie类的一个实例表示。一个cookie通常有一个名称、一个值集合和一个到期时间。此外,我们可以对cookie进行配置,以通过安全连接作用于特定的虚拟路径(例如, HTTPS)。
重要提示
ASP.NET 2.0利用HTTP-only特征建立支持会话cookie的浏览器上的会话cookie。例如,在Microsoft Internet Explorer 6.0 sp1或安装了Windows XP SP2的系统上支持会话cookie。HTTP-only特征防止这些cookie被用于客户端脚本,从而竖起一道屏障,以防可能发生的旨在偷取会话ID 的跨站点脚本 攻击。
当cookie被启用时,会话状态模块实际创建一个具有特定名称的cookie,并把会话ID保存在它那里。cookie的创建如下面的伪码所示:
HttpCookie sessionCookie;
sessionCookie = new HttpCookie("ASP.NET_SessionId", sessionID);
sessionCookie.Path = "/";
ASP.NET_SessionId是cookie的名称,而sessionID字符串是它的值。cookie也与当前域的根关联。Path属性描述应用cookie的相对URL。会话cookie被给予一个非常短的到期期限,并在每个成功的请求结束时续订。cookie的Expires属性指出该 cookie在客户端到期的时间。如果没有显式地设置该属性(会话cookie通常就是这样),则Expires属性默认为 DateTime.MinValue——即.NET Framework中可能的最小时间单位。
注意
需要写入一个cookie的服务器端模块添加一个HttpCookie对象到Response.Cookies集合中。客户端存在的并与被请求领域关联的所有cookie都被上传,并使其可以通过Request.Cookies集合读取。
3. 无cookie会话
为了使会话状态起作用,客户端必须能够把会话ID传递给服务器端应用程序。这种情况如何发生依赖于应用程序的配置。ASP.NET应用程序通过配置文件的<sessionState>节定义它们的会话特有的设置。为了决定cookie支持,将cookieless属性设置为表13.7中的一个值。表中所列的值属于HttpCookieMode枚举类型。
表13.7 HttpCookieMode枚举类型
模 式 |
描 述 |
AutoDetect |
只有在请求浏览器支持cookie时才使用cookie |
UseCookies |
使用cookie持久地存储会话ID,而不管浏览器是否支持cookie。这是默认选项 |
UseDeviceProfile |
根据配置文件的设备配置文件节所列的浏览器能力作出决策 |
UseUri |
将会话ID存储在URL中,不管浏览器是否支持cookie。如果在任何情况下都不使用cookie,则使用该选项 |
使用AutoDetect时,ASP.NET将查询浏览器以确定它是否支持cookie。如果浏览器支持cookie,会话ID就存储在一个 cookie中;否则,会话ID将存储在URL中。另一方面,使用UseDeviceProfile模式时,不检查浏览器的有效能力。为了让HTTP会话模块决定使用cookie还是URL,使用由HttpBrowserCapabilities对象的 SupportsRedirectWithCookie属性产生的浏览器所声明的功能。要注意的是,即使浏览器能够支持cookie,但是用户可能会禁用 cookie。在这种情况下,会话状态不能正确起作用。
注意
在ASP.NET 1.x中,可以选择的方案更少。<sessionState>节的cookieless属性只能接收一个布尔值。若要禁用会话中的cookie,则将该属性设置为true。
在禁用cookie支持下,假设请求如下URL处的一个页面:
http://www.contoso.com/test/sessions.aspx
在浏览器的地址栏中显示的内容略有不同,而且现在包含会话ID,如下所示:
http://www.contoso.com/test/(S(5ylg0455mrvws1uz5mmaau45))/sessions.aspx
实例化时,会话状态模块检查cookieless属性的值。如果该值为true,则请求被重定向(HTTP 302状态码)到一个修改过的虚拟URL(正好在页面名称前包含会话ID)。再次被处理时,该请求嵌入会话ID。一个专用的ISAPI筛选器 (aspnet_filter.exe组件)预处理该请求,解析URL,并在该URL融入一个会话ID时重写正确的URL。所检测到的会话ID也存储在一个称为AspFilterSessionId的额外HTTP头部,并在以后进行检索。
4. 无cookie会话的问题
无cookie会话旨在使有状态的应用程序也可以在一个不支持cookie的浏览器或者在一个没有启用cookie的浏览器上运行,但它也存在问题。首先,当会话开始时,以及每当用户从一个应用程序的页面内跟随一个绝对URL时,它们会导致重定向。
使用cookie时,我们可以清除地址栏,进入另一个应用程序,然后转向前一个应用程序,并检索相同的会话值。如果在禁用会话时这么做,会话数据会丢失。该特征对于页面回发没有什么问题,因为这是使用相对URL自动实现的,但若使用绝对URL进行链接,则会引发一个严重的问题。在这种情况下,总是会创建一个新的会话。例如,如下代码中断会话:
<a runat="server" href="/test/sessions.aspx">Click</a>
有没有一种办法自动打破链接或超链接中的绝对URL,以便它们融入会话信息吗?我们可以使用如下技巧,它使用了HttpResponse类的ApplyAppPathModifier方法:
<a href='<% =Response.ApplyAppPathModifier("test/page.aspx")%>' >Click</a>
ApplyAppPathModifier方法取一个表示相对URL的字符串,并返回一个绝对URL,其中嵌入会话信息。需要从一个HTTP页面重定向到一个HTTPS页面(其中强制要求完整的绝对地址)时,则上述技巧特别有用。要注意的是,如果会话cookie被启用,并且路径是绝对路径,则 ApplyAppPathModifier返回原始URL。
警告 在服务器端表达式(即标记了runat=server属性的表达式)中,不能使用<%...%>代码块。之所以在上述代码中可行,是因为<a>标签被逐字发出,没有设置runat属性。
5. 无cookie会话和安全性
使用无cookie会话的另一个问题与安全性有关。会话欺诈是最流行的攻击类型之一,牵涉到通过为另一个合法用户生成的会话ID访问外部系统。请尝试下面的做法:将应用程序设置为在无cookie下工作,并访问一个页面。当会话ID在浏览器的地址栏中出现时,获取它的URL,并立即通过电子邮件发给一位朋友。让你的朋友把URL粘贴到自己的机器上并单击“转到”。只要会话有效,你的朋友就能访问你的会话状态。毫无疑问,会话ID并没有得到良好的保护。为了系统的安全性,关键是有一个不可预测的ID生成器,因为它使猜测有效的会话ID更难了。对于无cookie会话,会话ID在地址栏中提供给外界,并且对所有的人都是可见的。由于这个原因,如果把专用的或机密的信息存储在会话状态中,建议使用安全套接字层(Secure Sockets Layer,SSL)或传输层安全性(Transport Layer Security,TLS)加密浏览器和服务器之间的包含会话ID的任何通信。
此外,当用户认为这么做违反了安全性时,始终应当使他们能够退出登录,并调用Abandon方法。对使用会话ID来窃取会话状态中所存储数据的人来说这种设计减少了他们的时间,而说到安全性,重要的是在使用无cookie会话时要对系统进行配置,以免重用过期的会话ID。此行为在ASP.NET中可以通过<sessionState>节进行配置,下一节将详细 介绍。
6. 配置会话状态
从ASP.NET 1.x迁移到ASP.NET 2.0以后,<sessionState>节的内容显著增多了。该节的内容如下所示:
<sessionState
mode="Off|InProc|StateServer|SQLServer|Custom"
timeout="number of minutes"
cookieName="session cookie name"
cookieless="http cookie mode"
regenerateExpiredSessionId="true|false"
sqlConnectionString="sql connection string"
sqlCommandTimeout="number of seconds"
allowCustomSqlDatabase="true|false"
useHostingIdentity="true|false"
partitionResolverType=""
sessionIDManagerType="custom session ID generator"
stateConnectionString="tcpip=server:port"
stateNetworkTimeout="number of seconds"
customProvider="custom provider name">
<providers>
...
</providers>
</sessionState>
表13.8详细描述了各个属性的目标和特征。要注意,只有mode,timeout,stateConnectionString和 sqlConnectionString属性在ASP.NET 1.x中有对等的属性。cookieless属性在ASP.NET 1.x中也存在,但是它接受布尔值。所有其他属性都是ASP.NET 2.0新引入的。
表13.8 <sessionState>属性
属 性 |
描 述 |
allowCustomSqlDatabase |
如果为true,则能够指定一个定制数据库表来存储会话数据,而不必使用标准的ASPState |
Cookieless |
指定如何把会话ID传递给客户端 |
cookieName |
cookie的名称(如果cookie用于会话ID的话) |
customProvider |
定制的会话状态存储提供程序的名称,用于存储和检索会话状态数据 |
mode |
指定把会话状态存储在哪里 |
partitionResolverType |
当会话状态工作于SQLServer或StateServer模式时,指明被加载以提供连接信息的分区解析器组件的类型和程序集。如果可以正确地加载分区解析器,则忽略sqlConnectionString和stateConnectionString属性 |
regenerateExpiredSessionId |
用一个已过期的会话ID发出一个请求时,如果该属性为true,则生成一个新的会话ID;否则,重新使用过期的会话ID。默认值为false |
sessionIDManagerType |
默认为Null。如果设置了该属性,则指明用作会话ID的生成器的组件 |
sqlCommandTimeout |
指定一个SQL命令在被取消前可以空闲的秒数。默认为30秒 |
sqlConnectionString |
指定SQL Server的连接字符串 |
stateConnectionString |
指定用来远程存储会话状态的服务器名称或地址和端口 |
stateNetworkTimeout |
指定Web服务器和状态服务器之间的TCP/IP网络连接在请求被取消前可以空闲的秒数。默认为10秒 |
timeout |
指定一个会话在被放弃前可以空闲的秒数。默认为20秒 |
useHostingIdentity |
默认值为True。它指明在访问一个定制的状态提供程序或者进行了集成安全性配置的SQLServer提供程序时,ASP.NET进程标识被假冒 |
此外,子<providers>节列出了定制的会话状态存储提供程序。ASP.NET会话状态旨在使我们能够轻松地把用户会话数据存储在不同的源中,如Web服务器的内存中或SQL Server。存储提供程序是一个管理会话状态信息的存储并把它存储在另一种介质(例如,Oracle)或布局中的组件。我们将在本章后面重新讨论该主题。
13.3.2 会话的生命期
只有在第一个数据项添加到内存中的会话字典时,会话状态的生命才开始。如下代码说明了如何修改会话字典中的一个数据项。“MyData”是惟一地标识该值的键。如果该字典中已经存在一个称为“MyData”的键,则重写现有的值:
Session["MyData"] = "I love ASP.NET";
Session字典通常包含object类型;要读回数据,需要将返回值转换为一种更具体的 类型:
string tmp = (string) Session["MyData"];
当页面把数据保存到Session字典中时,返回值被加载到一个内存中的字典——即,SessionDictionary内部类的一个实例(参见图13.2)。其他并发运行的页面不能访问该会话,直到正在进行的请求完成为止。
1. Session_Start事件
会话启动事件与会话状态无关。当会话状态模块服务给定用户发出的要求新会话ID的第一个请求时,Session_Start事件激发。ASP.NET运行库可以在一个会话上下文中服务多个请求,但是只对它们中的第一个请求激发Session_Start事件。
每当请求一个不把数据写入会话字典中的页面时,创建一个新的会话ID,并激发一个新的Session_Start事件。会话状态的体系结构非常复杂,因为它必须支持各种状态提供程序。总体方案要求在请求完成时把会话字典的内容序列化到状态提供程序。然而,为了优化性能,只有在字典的内容不空时该过程才会真正执行。然而,如前所述,如果应用程序定义了一个Session_Start事件处理程序,则无论如何都会发生序列化。
2. Session_End事件
Session_End事件表示会话的结束,用来执行终止会话所需的任何清除代码。然而要注意的是,只有在Inproc模式下,即当会话数据存储在ASP.NET工作进程中时,才支持该 事件。
为了使Session_End事件激发,会话状态必须先存在。这就是说,我们必须把一些数据存储在会话状态中,并且至少必须完成一个请求。第一个值添加到会话字典中时,一个数据项插入ASP.NET缓存——上述的Cache对象将在下一章详细介绍。该行为是进程中的状态提供程序所特有的;进程外状态提供程序和SQL Server状态服务器都不使用Cache 对象。
然而,更有趣的是,添加到缓存中的数据项(每个活动会话只有一个数据项)被赋予一个特殊的到期策略。我们将在下一章学习ASP.NET缓存及其相关的到期策略。而现在,只要知道添加到该缓存中的会话状态项被赋予一个活动到期时间,其间隔时间设置为会话超时时间。只要在会话内还有请求要处理,则滑动到期期限自动续订。会话状态模块在处理EndRequest事件时重置超时时间。只要通过对缓存执行一次读取操作,它就能获得期望的结果!给定 ASP.NET Cache对象的内部结构,这就等于续订滑动到期期限。因而,当缓存项到期时,该会话已经超时。
到期的缓存项自动地从缓存中删除。作为该项的到期策略的一部分,状态会话模块还指示一个删除回调函数。缓存自动地调用该删除方法,而后者激发Session_End事件。
注意
Cache中表示一个会话的状态的数据项,不能从system.web程序集的外部进行访问,更不能进行列举,因为它们被存放在一个系统保留的缓存区域。换句话说,我们不能以编程的方式访问驻留在另一个会话中的数据,更不能删除它。
3. 为什么我的会话状态会丢失?
当会话超时或被放弃时,Session对象中存储的值要么以编程的方式从内存中删掉,要么被系统从内存中删掉。然而,在某些情况下,会话状态会不知不觉地丢失。如何解释这种奇怪的行为呢?
当工作模型是InProc时,会话状态在正在服务该页面请求的AppDomain的内存空间中被映射。根据该规定,会话状态受进程回收和 AppDomain重启支配。ASP.NET工作进程定期重启,以维护良好的平均性能;当ASP.NET工作进程重启时,会话状态丢失。进程回收取决于内存消耗百分比,还有可能取决于被服务的请求量。虽然这是周期性的,但是不能对周期的间隔时间做出一般的估计。在设计基于会话的、进程内应用程序时,要注意这一点。通常要记住的是,试图访问会话状态,它可能没有。尽量使用适合自己的应用程序的异常处理或还原技术。
考虑到一些反病毒软件可能把web.config或Global.asax文件标记为已修改,导致一个新的应用程序启动,从而丢失会话状态。如果我们或我们的代码修改那些文件的时间戳,或者修改其中一个专用文件夹(诸如Bin或App_Code等)的内容,也会丢失会话状态。
注意
当一个正在运行的页面碰到一个错误时,会话状态会发生什么呢?在请求结束时,如果页面产生一个错误——即,Server对象的 GetLastError方法返回一个异常,则该会话的状态不会被保存。然而,如果在异常处理程序中通过调用Server.ClearError重置错误状态,则好像没有发生任何错误一样有规律地保存会话的值。