会话状态处理任务可用三个步骤来概括:分配会话ID,从提供程序获取数据,将数据填充到页面的上下文中。
会话状态模块负责管理所有这些任务的执行。为此,它还需要利用两个组件:会话ID生成器和会话状态提供程序。在ASP.NET 2.0和更高版本中,二者可以由自定义的组件代替。
会话的标识
每个活动的ASP.NET会话由15个字节(120位)的字符串标识,其中只包含符合URL标准的字符。会话ID是随机生成的,且是唯一的,以避免恶意攻击和数据冲突。通过某种算法从一个现有的ID推算出一个有效的会话ID几乎是不可能的。
会话ID的生成
会话ID的长度为15个字节,由“随机数字生成器(RNG)”密码提供程序生成。该服务提供程序能返回15个随机生成的数字序列,这个数字数组之后被映射为有效的URL字符,并以字符串的形式返回。
如果会话中未包含任何数据,那么每个请求都会获得新生成的会话ID,但会话状态不会由状态提供程序保存。然而,如果设置了Session_Start事件的处理程序,会话状态总会被保存(即使它为空)。因此,定义Session_Start事件的处理程序应谨慎,不到万不得已不要这么做,特别是在不使用进程内会话提供程序时。
相反,如果是非空会话字典,那么在其超时或被放弃后,会话ID仍会被保留。根据设计,即使会话状态已过时,会话ID仍会持续到浏览器会话结束。这意味着,只要浏览器实例不变,同一个会话ID就会用于表示多个同时存在的会话。
会话Cookie
在浏览器与服务器之间,SessionID字符串以两种方式传递:使用Cookie,或使用修改的URL。默认情况下,会话状态模块会在客户端创建HTTP Cookie,但修改的URL可以嵌入SessionID字符串(这种方式特别适合无Cookie浏览器的情况)采用哪种方法取决于存储在web.config文件中的设置,默认会采用Cookie方案。
实际上,Cookie无非是一种位于客户端硬盘上与Web页面关联的文本文件。在ASP.NET中,Cookie由HttpCookie类实例表示。通常,Cookie数据保护名称、值的集合和过期时间。此外,我们还可配置Cookie的虚拟路径,还可选择是否通过安全连接(如HTTPS)传输。
ASP.NET 2.0及更高版本利用了浏览器HTTP-only的会话Cookie支持。安装IE 6.0 SP1和WinXP SP2的计算机都支持。HTTP-only功能能够防止客户端脚本使用Cookie,从而降低了为窃取会话ID来进行跨站点脚本攻击的潜在危险。
若启用Cookie,会话状态模块会创建一个带有特定名称的Cookie,并将会话ID存入其中。该Cookie的创建过程如下:
ASP.NET_SessionId是这个Cookie的名称,SessionID字符是值。Cookie还与当前域的根关联。Path属性用于描述Cookie的相对URL。会话Cookie会被分配一段非常短暂的过期时间,并在每个请求成功地结束后更新。Cookie对象的Expires属性用于指示客户端Cookie过期的天数。如果没有显式的设置,与会话Cookie一样,Expires属性默认为DateTime.MinValue,即.Net中定义的最短时间。
要写入Cookie的服务器端模块,需要向Response.Cookies集合添加HttpCookie对象。在客户端发现的所有与被请求域关联的Cookie都会被上传到服务器,随后可通过Request.Cookies集合读取。
无Cookie会话
为使会话状态正常工作,客户端必须能够将会话ID上传至服务器端应用程序。这个过程具体细节取决于应用程序的配置。ASP.NET应用程序通过配置文件的<sessionState>定义会话特有的设置。会确定Cookie的支持方式,我们需要将cookieless属性设置为下表中的某个值,这些值为HttpCookieMode枚举类型:
在禁用Cookie支持的情况下,假设用户通过这样的URL来请求某个页面:
浏览器地址栏中显示的地址会发生一些变化的(其中包含会话ID),如下所示:
当会话状态模块实例化时,它会检查cookiesless属性值,如果发现Cookie被禁用,请求会被重定向到一个被修改的虚拟URL(HTTP状态码为302),其中包含一个会话ID,刚好在页面名的前面。当再次开始处理请求时,这个会话ID会嵌入请求中。这个请求会由一个特殊的ISAPI筛选器(aspnet_filter.exe组件)做预处理--解析其URL,若带有会话ID,则将其重写为正确的URL。被检测到的ID会存储在单独的HTTP标头(AspFilterSessionId)中,以便稍后使用。
无Cookie会话带来的问题
当会话开始时,不论用户发出的是否为应用程序页面的绝对URL,无Cookie都会引发重定向。
若使用Cookie,且在地址栏填入另一个应用程序的地址,那么在返回之前的页面时,获取的是相同的会话值。如果禁用Cookie,当页面回发时会自动通过相对URL来实现,不受影响。但如果使用绝对URL链接,则会造成会话数据则会丢失,这种情况下,总会创建新的会话。例如下面的代码就会打断当前会话:
有什么办法能够自动修改链接和超链接中的绝对URL,使其融入会话ID信息?
我们可以使用HttpResponse类的ApplyAppPathModifier方法:
ApplyAppPathModifier方法接受一个代表相对URL的字符串,返回的是带有会话信息的绝对URL。这个技巧非常适合将HTTP页面重定向到HTTPS页面,这时必须使用完整的绝对地址。注意,如果会话Cookie是启用的,或传入的路径是绝对路,那么ApplyAppPathModifier返回的则是原始的URL。
我们不能在服务器端表达式(即,带有runat=server属性的表达式)中使用<%...%>代码块。该代码块之所以能在上述代码工作是因为<a>标签是纯文本,没有runat属性。注意,这里所说的代码块与数据绑定表达式<%#...%>没有任何关系。之所以不能在服务器端表达式中使用<%...%>代码块,是因为runat属性会指示将当前标签强制创建为服务器对象,而服务器对象不会处理这种代码块。
无Cookie会话与安全
使用无Cookie会话引发的另一个问题与安全有关。会话劫持(Session Hijacking)是最常见的攻击类型之一,它通过生成另一个合法用户的会话ID来访问外部系统。为理解这种攻击方式,可以这样做:将应用程序配置为不使用Cookie,并访问某个页面,获取带有会话ID的URL(可取自浏览器的地址栏),并立即通过电子邮件发送给一个您的朋友。让对方将该URL粘贴到自己浏览器的地址栏中,并单击“转到”。只要您的会话还是活动的,那么您的朋友也能访问相同的会话。
出于对系统安全的考虑,生成随机ID很关键,因为这会使攻击者很难猜测到有效的会话ID。对于无Cookie会话,会话ID暴露在地址栏中,对外界可见。因此,如果要将私人或敏感信息存储在会话中,建议使用安全嵌套字层(Secure Sockets Layer,SSL)或传输层安全性(Transport Layer Security,TLS)对浏览器和服务器间包含会话ID的通信进行加密。
此外,若用户认为这样会降低安全性,我们还应为其提供注销功能,并调用Abandon方法。这样会缩短在某个攻击者设法找到合法用户的会话ID的时间。而且从安全性角度来讲,在使用无Cookie会话时,有必要对应用程序进行配置,使其避免重复使用过期的会话ID。在ASP.NET中,该行为可通过<sessionState>区段进行配置。
会话状态的配置
在ASP.NET 1.x向ASP.NET 2.0过渡的过程中,<sessionState>区段的选项也随之增加,如下所示:
sessionState的属性说明见下表:
此外,子区段<providers>用户设置所有自定义会话状态存储提供程序。
会话的生存期
会话状态的生存期起始于首个数据项被添加到内存中的字典时。该字典是一个SessionDictionary的内部类实例。
Session_Start事件
会话启动事件与会话状态无关。Session_Start事件将在会话状态模块为用户的首个请求提供服务且需要新会话ID时引发。ASP.NET运行库能在单个会话上下文中为多个请求服务,但只有第一个请求会引发Session_Start事件。
若不向字典中写入数据,便会在请求页面时创建新的会话ID并引发Session_Start事件。
Session_End事件
Session_End事件用于通知会话的结束,并执行终止会话涉及的清理代码。但应注意,该属性要求当前处于InProc模式下(会话数据存储在ASP.NET工作线程)。
为使Session_End引发,会话状态必须事先已经存在。这意味着,我们必须在会话状态中存储一些数据,且至少完成一个请求。当第一个值被添加到会话字典中时,会有一个对应项被插入ASP.NET缓存。该行为针对的是进程内状态提供程序,进程外状态服务器和SQL Server状态服务都不涉及Cache对象。
添加到缓存的会话状态项会被指定一个可调的过期时间(会话超时设置中的间隔时间),只要有请求在当前会话上下文中处理,可调的时间会自动更新。会话状态模块会在处理EndRequest事件时重置这个超时设置。该模块只要对缓存执行一次读取操作,就能达到期望的效果。ASP.NET Cache对象的内部结构,使其能够估算出该可调时间段的长短。因此,当缓存项过期时,会话状态也已超时。
过期项会自动从缓存中移除。作为过期策略的一部分,会话状态模块还会指定一个移除回调函数。缓存对象会自动调用该移除函数,而该函数会引发Session_End事件。
Cache中代表会话状态的项无法在system.web程序集之外访问,更不能进行枚举,因为它们被置于缓存的系统保留区域内。也就是说,我们不能以编程的方式访问位于另一个会话中的数据,移除就更谈不上了。
为什么会话状态有时会丢失
Session对象中的值可以编程方式移除,也会在会话超时或被放弃时由系统移除。但在某些情况下,会话状态会莫名其妙的丢失,这是为什么呢?
当处在InProc模式下时,会话状态会被映射到处理当前请求的AppDomain内存空间中。因而,会话状态会受进程回收和AppDomain重启的影响。ASP.NET工作线程会周期性的重启以便保持总体上的良好性能,重启后,会话状态便会丢失。进程回收的执行会根据内存的占用率和被服务的请求数。虽然该过程是周期性的,但无法估计出回收的间隔。因此,在设计基于会话的、进程内的应用程序时,应充分考虑这个问题。会话状态可能在试图访问时不存在。使用异常处理,还是使用恢复技术,要根据具体的应用程序来分析。
当某页面运行出现错误时,会话状态会受到怎样的影响?当前的会话字典是会被保存还是丢失?若页面在请求结束时发生错误(Server对象的GetLastError方法返回一个异常对象),会话状态则不会被保存。然而,如果在异常处理程序中通过调用Server.ClearError方法来重置异常状态,那么会话状态数据便会按常规方式保存,就像没有发生错误一样。
将会话状态保存在远程服务器中
对于前面提到的InProc模式下会话状态会丢失的问题,可以利用进程外状态提供程序来解决。但如果这样的话,会话状态不存储在ASP.NET工作线程中,需要一层额外的代码来对存储介质中的数据进行序列化和反序列化,该操作发生在请求被处理期间。
将会话数据从外部存储区复制到本地会话字典中,可能造成状态管理进程的性能下降15%--25%。
若选择进程外状态提供程序,应考虑在应用程序进入生产环境前事先建立运行时环境。这涉及启用有关StateServer的Windows服务或配置SQLServer数据库。
状态的序列化和反序列化
若选择InProc模式,存储在会话状态中的对象是类的实例,无需执行序列化或反序列化。我们可以在会话状态中存储开发者所创建的任何对象,访问它们也不会有额外的开销。但如果选择进行外状态提供程序,情况则迥然不同。
在进程外架构中,会话值需要从原始的存储介质复制到处理请求的AppDomain的内存中。这方面的工作需要序列化/反序列化来完成,这是进程外状态提供程序主要的开销之一。这么我们的代码有何影响?首先,我们应确保字典中只存储可序列化的对象。
在序列化和反序列化方面,ASP.NET中有两种方式,性能各有不同。对于基本的类型,ASP.NET会使用经优化的内部序列化程序;而对于其他类型,ASP.NET会使用.NET二进制格式化程序,其速度较慢。
经优化的序列化程序(一个叫AltSerialization的内部类)会使用BinaryWriter对象,先写入代表相应“类型”的字节,然后才是它的值。在读取时,AltSerialization类首先会抽取一个字节,判断要读取的数据的类型,然后借助BinaryReader类中类型特定的方法进行读取具体的值。每个类型会根据一个内部的表与一个索引值关联。
布尔类型和数据类型的大小是固定的,而字符串的长度却是可变的。那么,读取器是如何确定字符串的长度呢?BinaryReader.ReadString方法利用了这样一个技巧:在底层流中,字符串总带有一个长度前缀。对于DataTime类型,该方法会通过日期和时间的总刻度数的形式写入,作为Int64类型读取。
对于复杂对象,只要它被标记为“可序列化”,则会通过BinaryFormatter类对其进行序列化,其速度相对较慢。简单类型和复杂类型都会使用同一个流,但所有非基本类型由同一个类型ID来标识。
我们不应该将任何对象都存储在Session中。如果使用进程外方案,对DataSet的存储应该谨慎。这与DataSet类的序列化过程有关。由于DataSet是复杂类型,它是通过二进制格式化程序进行序列化的。而DataSet本身的序列化会生成许多XML数据,而这会对应用程序造成严重的缺陷,尤其对于存储大量数据的大型应用程序。
会话数据的存储
若工作在StateServer模式下,整个HttpSessionState对象的内容会被序列化到一个外部应用程序,即一个名为aspnet_state.exe的Microsoft Windows NT服务。该服务用于在请求结束时对会话状态进行序列化。在内部,该服务使用字节数组来存储每个会话状态。若开始处理新的请求,与给定会话ID对应的数组会被复制到内存流中,然后被反序列化成内部的会话状态项对象。该对象代表整个会话的内容。页面实际使用的HttpSessionStae对象只是其应用程序编程接口。
StateServer提供程序的配置
若采用进程外存储方案,会话状态的生存期便会被延迟。这样一来,应用程序的健壮性更强。通过将会话状态与页面分离,我们还能够将现有的应用程序逐步向Web Farm和Web Garden架构转化。
ASP.NET会话状态提供程序是一个叫aspnet_state.exe的Windows服务,该服务的可执行文件位于ASP.NET的安装文件夹:%Windows%\Microsoft.Net\Framework\[version]。
注意,具体的路径取决于实际运行的.NET Framework的版本。在使用状态服务前,应确保该服务在本地或用于存储会话的远程计算机上正常运行。
我们需要为asp.net应用程序指定运行会话状态服务的计算机的IP地址。为启用远程会话状态,我们需要在web.config中进行配置:
服务器名既可以是IP地址,也可以是计算机名称。
状态服务器不会为请求者设置任何身份验证障碍,这样,网络用户可以自由访问会话数据。为保护会话状态,应确保其只能接受来自Web服务器计算机的访问。为此,我们可以使用防火墙、IPSec策略或安全网络10.X.X.X,这样,外部攻击都便不能直接访问了。我们还可以修改其服务端口,修改注册表中的键值“HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\aspnet_state\Parameters”即可修改状态服务的端口。
ASP.NET应用程序会在加载后立即尝试连接到会话状态服务器。状态服务会使用.NET Remoting进行数据通信。
默认情况下,状态服务器只监听本地连接,如果状态服务器和Web服务器是不同的计算机,我们需要启用远程连接。为此,我们只要在注册表中修改键值“HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\aspnet_state\AllowRemoteConnection”,将其设置一个非0值即可。
将数据保存到SQL Server
如果应用程序对健壮性要求较高,比如状态提供服务被停用后,数据仍然不会丢失,不妨将SQL Server服务。
若ASP.NET工作在SQL Server模式下,会话数据存储在一个专用的数据库表中,因此,即使SQL Server崩溃,会话数据也不会丢失,但这需要更高的系统开销。
为将SQL Server作为状态提供程序,需要对web.config文件进行配置:
如果不通过allowCustomSqlDatabase启用自定义数据库,那么该连接字符串中不能包含Database和Initial Catalog这样的设置。仅当allowCustomSqlDatabase设置被启用时,我们才可通过Database和Initial Catalog指定数据库名称。
连接字符串也可以引用定义在<connectionString>区段中的连接字符串,连接字符串名称可以在<sessionState>相应属性中指定。
对数据库的访问凭据,我们可以通过用户ID和密码来提供,也可以利用“集成安全性”。不论使用哪个帐户来访问SQL Server中的会话状态,都应确保用户至少拥有db_datareader和db_datawriter权限。还应注意,为配置存储会话状态的SQL Server环境,需要管理员权限,因为要创建新的数据库和存储过程。
对于SQL Server模式下的会话状态,我们能指定自定义命令的超时值(以秒为单位)sqlCommandTimeout属性来进行设置。
SQL Server数据库的创建
ASP.NET提供了两对用于配置数据库环境的脚本,以便创建必要的表、存储过程、触发器、作业等。
第一对脚本为InstallSqlState.sql和UninstallSqlState.sql。它们能够创建一个ASPState数据和几个存储过程,但数据存储在TempDB数据库的几个表中。这样,如果SQL Server所处的计算机被重启,会话数据将丢失。
另一对脚本为InstallPerisistSqlState.sql和UninstallPerisistSqlState.sql。与第一对脚本的不同之处是,它们创建的表在ASPState数据库中,是持久性的。共有两个表,表名分别为ASPStateTempApplications和ASPStateTempSessions。
所有脚本可以在以下路径中找到:
%SystemRoot%\Microsoft.NET\Framework\[version]
包含这些脚本文件只是为了向后兼容,我们应使用aspnet_regsql.exe来安装和卸载SQL会话状态。
当前运行的每个ASP.NET应用程序对应于ASPStateTempApplications表中的一条记录。下表对表中各列做了说明:
ASPStateTempSessions表用于存储实际的会话数据,每个活动的会话对应于表中的一条记录,下表描述了该表的结构:
在安装会话的SQL Server支持时,还有一个作业被添加,它用于从会话状态数据库中删除过期的会话。该作业名称为ASPState_Job_DeleteExpiredSessions,默认配置是每分钟运行一次。