1. 保留默认的会话模块,但编写自定义的状态提供程序来更改存储介质。这样,我们还有机会重写一些用于在存储与session之间运载会话数据的辅助类。
public delegate void sessionstateitemexpirecallback(string sessionid, sessionstatestoredata item);
customprovider="samplesessionprovider">
type="samplesessionstateprovider, mylib" />
使用
会话状态模块负责管理所有这些任务的执行。为此,它还需要利用两个组件:会话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的创建过程如下: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中定义的最短时间。要写入cookie的服务器端模块,需要向response.cookies集合添加httpcookie对象。在客户端发现的所有与被请求域关联的cookie都会被上传到服务器,随后可通过request.cookies集合读取。无cookie会话为使会话状态正常工作,客户端必须能够将会话id上传至服务器端应用程序。这个过程具体细节取决于应用程序的配置。asp.net应用程序通过配置文件的定义会话特有的设置。会确定cookie的支持方式,我们需要将cookieless属性设置为下表中的某个值,这些值为httpcookiemode枚举类型: 在禁用cookie支持的情况下,假设用户通过这样的url来请求某个页面:http://www.contoso.com/test/sessions.aspx浏览器地址栏中显示的地址会发生一些变化的(其中包含会话id),如下所示:http://www.contoso.com/test/(s(sylgasdfueoruikfjoiueriljk))/sessions.aspx当会话状态模块实例化时,它会检查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属性的表达式)中使用<%...%>代码块。该代码块之所以能在上述代码工作是因为标签是纯文本,没有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中,该行为可通过区段进行配置。 会话状态的配置在asp.net 1.x向asp.net 2.0过渡的过程中,区段的选项也随之增加,如下所示: 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"
statenewworktimeout="number of seconds"
customprovider="custom provider name">
...
sessionstate的属性说明见下表:此外,子区段用户设置所有自定义会话状态存储提供程序。 会话的生存期会话状态的生存期起始于首个数据项被添加到内存中的字典时。该字典是一个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.netframework[version]。注意,具体的路径取决于实际运行的.net framework的版本。在使用状态服务前,应确保该服务在本地或用于存储会话的远程计算机上正常运行。我们需要为asp.net应用程序指定运行会话状态服务的计算机的ip地址。为启用远程会话状态,我们需要在web.config中进行配置:
stateconnectionstring="tcpip=mymachine:42424" />
服务器名既可以是ip地址,也可以是计算机名称。状态服务器不会为请求者设置任何身份验证障碍,这样,网络用户可以自由访问会话数据。为保护会话状态,应确保其只能接受来自web服务器计算机的访问。为此,我们可以使用防火墙、ipsec策略或安全网络10.x.x.x,这样,外部攻击都便不能直接访问了。我们还可以修改其服务端口,修改注册表中的键值“hkey_local_machinesystemcurrentcontrolsetservicesaspnet_stateparameters”即可修改状态服务的端口。asp.net应用程序会在加载后立即尝试连接到会话状态服务器。状态服务会使用.net remoting进行数据通信。默认情况下,状态服务器只监听本地连接,如果状态服务器和web服务器是不同的计算机,我们需要启用远程连接。为此,我们只要在注册表中修改键值“hkey_local_machinesystemcurrentcontrolsetservicesaspnet_stateallowremoteconnection”,将其设置一个非0值即可。将数据保存到sql server如果应用程序对健壮性要求较高,比如状态提供服务被停用后,数据仍然不会丢失,不妨将sql server服务。若asp.net工作在sql server模式下,会话数据存储在一个专用的数据库表中,因此,即使sql server崩溃,会话数据也不会丢失,但这需要更高的系统开销。为将sql server作为状态提供程序,需要对web.config文件进行配置:
sqlconnectionstring="server=127.0.0.1;integrated security=sspi;" />
如果不通过allowcustomsqldatabase启用自定义数据库,那么该连接字符串中不能包含database和initial catalog这样的设置。仅当allowcustomsqldatabase设置被启用时,我们才可通过database和initial catalog指定数据库名称。连接字符串也可以引用定义在区段中的连接字符串,连接字符串名称可以在 相应属性中指定。 对数据库的访问凭据,我们可以通过用户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.netframework[version]包含这些脚本文件只是为了向后兼容,我们应使用aspnet_regsql.exe来安装和卸载sql会话状态。当前运行的每个asp.net应用程序对应于aspstatetempapplications表中的一条记录。下表对表中各列做了说明:aspstatetempsessions表用于存储实际的会话数据,每个活动的会话对应于表中的一条记录,下表描述了该表的结构:在安装会话的sql server支持时,还有一个作业被添加,它用于从会话状态数据库中删除过期的会话。该作业名称为aspstate_job_deleteexpiredsessions,默认配置是每分钟运行一次。