<sessionstate sessionidmanagertype="samples.myidmanager, mylib" >
</sessionstate>
使用
工作階段狀態模組負責管理所有這些任務的執行。為此,它還需要利用兩個組件:會話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應用程式通過設定檔的<sessionstate>定義會話特有的設定。會確定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連結,則會造成會話資料則會丟失,這種情況下,總會建立新的會話。例如下面的代碼就會打斷當前會話:
<a runat="server href="/test/sessions.aspx">click</a>
有什麼辦法能夠自動修改連結和超連結中的絕對url,使其融入會話id資訊?
我們可以使用httpresponse類的applyapppathmodifier方法:
<a href='<% =response.applyapppathmodifier("test/page.aspx")%>' >click</a>
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
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">
<providers>
...
</providers>
</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對象只是其API。
stateserver提供者的配置
若採用進程外儲存方案,工作階段狀態的生存期便會被延遲。這樣一來,應用程式的健壯性更強。通過將工作階段狀態與頁面分離,我們還能夠將現有的應用程式逐步向web farm和web garden架構轉化。
asp.net工作階段狀態提供者是一個叫aspnet_state.exe的windows服務,該服務的可執行檔位於asp.net的安裝資料夾:%windows%microsoft.netframework[version]。
注意,具體的路徑取決於實際啟動並執行.net framework的版本。在使用狀態服務前,應確保該服務在本地或用於儲存會話的遠端電腦上正常運行。
我們需要為asp.net應用程式指定運行工作階段狀態服務的電腦的ip地址。為啟用遠端工作階段狀態,我們需要在web.config中進行配置:
<configuration>
<system.web>
<sessionstate mode="stateserver"
stateconnectionstring="tcpip=mymachine:42424" />
</system.web>
</configuration>
伺服器名既可以是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檔案進行配置:
<configuration>
<system.web>
<sessionstate mode="sqlserver"
sqlconnectionstring="server=127.0.0.1;integrated security=sspi;" />
</system.web>
</configureation>
如果不通過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.netframework[version]
包含這些指令檔只是為了向後相容,我們應使用aspnet_regsql.exe來安裝和卸載sql工作階段狀態。
當前啟動並執行每個asp.net應用程式對應於aspstatetempapplications表中的一條記錄。下表對錶中各列做了說明:
aspstatetempsessions表用於儲存實際的會話資料,每個活動的會話對應於表中的一條記錄,下表描述了該表的結構:
在安裝會話的sql server支援時,還有一個作業被添加,它用於從工作階段狀態資料庫中刪除到期的會話。該作業名稱為aspstate_job_deleteexpiredsessions,預設配置是每分鐘運行一次。