本文英文原版及代碼下載: http://www.asp.net/learn/security/tutorial-03-cs.aspx
Security Tutorials系列文章第三章:Forms Authentication Configuration and Advanced Topics
導言:
在上一章,我們探討了在ASP.NET應用程序了執行forms authentication所必須的步驟,如何在Web.config文件里指定from配置以創建一個登錄頁面,對認證用戶和匿名用戶分別顯示不同的內容.記得我們通過將<authentication>元素的mode屬性配置為Forms以使站點使用forms authentication.其實<authentication>元素還可以任意的包含一個<forms>子元素,通過該子元素,我們可以指定各類forms authentication配置.
在本文,我們將考察各種不同的forms authentication設置,以及如何通過<forms>元素來對它們進行修改.我們將詳細的考察如何定制表單驗證票據的timeout值,如何為登錄頁指定一個自定義URL(比如用SignIn.aspx替換掉默認的Login.aspx), 以及無cookie的表單認證票據.我們也將更加深入地考察表單驗證票據的結構(makeup),看ASP.NET如何來確保票據的數據的安全.我們也會考察如何在表單認證票據里存儲額外的用戶數據,以及如何通過一個自定義principal object對象來對數據進行模型化(model this data).
第一步:考察<forms>配置選項
ASP.NET里的forms authentication system提供了一系列的配置選項.比如表單驗證票據的生命周期;對票據實施何種保護;在何種條件下使用無cookie的票據;登錄頁面的路徑等等.為了修改這些默認的值,我們要在<authentication>元素里添加一個子元素<forms>,指定的屬性值要像XML屬性那樣,比如:
<authentication mode="Forms">
<forms propertyName1="value1"
propertyName2="value2"
...
propertyNameN="valueN" />
</authentication>
表1對可以通過<forms>元素進行定制的屬性進行了匯總,由于Web.config是一個 XML文件, 在表左邊的attribute名稱都是區分大小寫的.
Table 1: <forms>元素屬性匯總
在ASP.NET 2.0及更高版本,默認的forms authentication值都是在.NET Framework里的FormsAuthenticationConfiguration類里“硬編碼”的.任何的修改都應在Web.config里依據application-by-application的原則進行.這與ASP.NET 1.x不同,在ASP.NET 1.x里,默認的forms authentication值是存儲在machine.config文件里的(因此可以通過machine.config來進行修改).關于這一點,有必要提一下,ASP.NET 1.x里的很多forms authentication配置與ASP.NET 2.0及更高版本是不同的.如果你打算將應用程序從ASP.NET 1.x環境里遷移過來,知曉這些差異是很重要的.對這些差異,你可以查閱《the <forms> element technical documentation》.
注意:
有幾個forms authentication配置,比如timeout, domain, 以及path,都對最終的表單認證票據cookie進行了詳細的指定.關于cookies的更多信息,比如它們是如何進行工作的,它包含的哪些屬性等,請查閱系列文章《Cookies tutorial》.
指定票據的Timeout值
表單認證票據就是用戶身份的憑證(ticket).對基于cookie的票據而言,該憑證是以cookie的形式進行存儲的,每次發出請求都會將該憑證發送到web server.從本質上來說,擁有了憑證,就相當于向系統宣稱:“我是xxx,我已經登錄,通過認證了”,在用戶在頁面間導航時記住用戶的身份(identity).
表單驗證票據不僅包含了用戶的身份,還包含了其它的信息以確保憑證的真實性和安全性.畢竟,我們并不希望一個懷有惡意的用戶創建一個假憑證或對某個真實有效的憑證秘密的進行修改.
票據包含的眾多信息里面有一個叫做expiry(可理解為有效期),它是票據過期的日期和時間點.每次FormsAuthenticationModule對一個票據進行檢查時都要確保其沒有過期,如果過期了就銷毀票據,將用戶看作匿名用戶.這種安全措施可以抵御重發攻擊(replay attacks).沒有expiry的話,如果某個黑客截獲了一個有效的認證票據,它們就可以用這個偷來的票據向服務器發送一個請求,以通過認證,達到登錄的目的.雖然expiry不能預防這種情況的發生,但它可以對這種攻擊可以成功的窗口進行限制(it does limit the window during which such an attack can succeed.).
注意:
第3步將詳細介紹forms authentication用于保護票據的技術.
當創建票據后,forms authentication system通過參考timeout設置來判斷它的expiry.正如Table 1里提到的那樣,默認設置是30分鐘,意思就是說,票據在其創建之時起的30分鐘內有效.
這樣定義的是票據的絕對的過期時間,但開發人員通常希望執行的是靈活的過期時間,也就是說,在每次用戶向站點發出請求時,只要票據還沒有過期就重新設置過期時間.為此,我們要通過slidingExpiration配置選項來實現該行為.如果將它設置為true,每次FormsAuthenticationModule對用戶通過認證后,就更新該用戶的票據的expiry.如果設置為false (默認值), 就不對expiry進行更新,后果就是,用戶票據一旦過了設置好的絕對過期點,用戶就從認證用戶變成匿名用戶.
注意:
票據的expiry信息是一個絕對的日期和時間值,比如“August 2, 2008 11:34 AM.” 然而,該日期和時間值又與服務器所在的本地時間有關.我們假定服務器所在的區域采用的時間標準是Daylight Saving Time(DST)——也就是美國地區的時間要向前推進一個小時 (注:不知這樣翻譯是否恰當,地理知識忘得差不多了,翻譯有誤請指正!).我們來看看某個ASP.NET站點在DST執行時間(也就是2:00 AM)左右會發生什么有趣的事情.假定某個用戶在March 11, 2008 1:55 AM登錄網站,那么站點將會為他創建一個票據,該票據過期時間為March 11, 2008 2:25 AM(因為采用的是默認的30分鐘).然而,到了2:00 AM,因為采用的是DST時間標準的緣故,時間自動跳到3:00 AM.當用戶在登錄6分鐘后(也就是在3:01 AM)訪問一個新頁面時,FormsAuthenticationModule發現票據已經過期了,因此將用戶重新導航到登錄頁面.關于這點以及票據的timeout的稀奇古怪的事情的探討,請參閱Stefan Schackow所著的《Professional ASP.NET 2.0 Security, Membership, and Role Management》(ISBN: 978-0-7645-9698-8).
圖1闡述的是將slidingExpiration設置為false,且timeout為30時的工作流程.注意,登錄是創建的票據包含了票據的有效期,且在以后的請求發生時不對有效期進行更新.如果FormsAuthenticationModule發現票據過期了就摒棄它,并將用戶當成匿名用戶.
圖1
圖2顯示的是slidingExpiration設置為true,且timeout設置為30時的工作流程.當一個認證用戶的請求抵達時(且該用戶的票據未過期),就對過期時間進行更新.
當使用基于cookie的票據時(這是默認的),問題就變的稍微復雜了點,因為cookie也有自己的expiry,它指示瀏覽器該在什么時候銷毀cookie.
如果沒有為cookie指定expiry,在關閉瀏覽器時就會自動銷毀cookie.如果有expiry,cookie就存儲在用戶的電腦里,超過指定的日期和時間時就會被銷毀.當某個cookie被銷毀掉了,就再也不會將它發送給服務器了.
注意:
當然,我們也可以提前銷毀存儲在電腦里的任何cookies.在Internet Explorer 7里,你點“工具”、“選項”、“瀏覽器歷史記錄”區域里的“刪除”按鈕,再點“刪除cookies”按鈕即可.
forms authentication system是創建基于session還是基于expiry(session-based or expiry-based)的cookies 呢?這要取決于傳遞給persistCookie參數的值.在前一篇文章里,我們提到FormsAuthentication類的GetAuthCookie(), SetAuthCookie(),和RedirectFromLoginPage()方法都包含2個參數:username和 persistCookie.另外,我們創建的登錄頁面包含一個“Remember me” CheckBox,通過它來判斷是否創建一個“持久保存的cookie”(Persistent cookies).“持久保存的cookie”是基于expiry的;而“非持久保存的cookie”是基于session的.
不管是基于session還是基于expiry的cookies,前面探討的timeout 和 slidingExpiration概念應用到這2種cookies時都是一樣的.唯一比較大的區別在于執行方面:對使用基于expiry的cookies而言,如果將slidingTimeout設置為true的話,當指定的有效時間長度超過一半時(比如,如果指定的有效期為20分鐘,當時間超過10分數時)才對cookie的expiry進行更新.
我們來對站點的票據過期策略進行改動,使票據在一個小時(60分鐘)后過期,且使用sliding expiration.為此,改動Web.config文件,為
<authentication>元素添加一個<forms>子元素,設置如下:
<authentication mode="Forms">
<forms slidingExpiration="true" timeout="60" /> </authentication>
使用定制的登錄頁面URL,而不是默認的Login.aspx
由于FormsAuthenticationModule自動將未授權的用戶導航到登錄頁面,因此它需要知道登錄頁面的URL.該URL通過<forms>元素的loginUrl屬性來指定,默認值為“login.aspx”.
比如你的系統的登錄頁面為SignIn.aspx,且位于Users目錄下,那么你可以將 loginUrl設置為“~/Users/SignIn.aspx”,如下:
<authentication mode="Forms">
<forms loginUrl="~/Users/SignIn.aspx" />
</authentication>
當然,如果你的系統的登錄頁面就是Login.aspx,那就用不著在<forms>元素里進行指定了.
第二步:使用無Cookie的表單認證票據
默認情況下,forms authentication system將決定是將票據存儲在cookies collection里還是插入用戶訪問頁面的URL里.所有主流的桌面瀏覽器,比如Internet Explorer,Firefox, Opera, 或Safari都支持cookies,但并非所有的移動設備都支持.
forms authentication system使用何種cookie策略,取決于<forms>元素里的cookieless設置,它可以有如下四種配置:
.UseCookies——指定總是使用基于cookie的票據
.UseUri——指定從不使用基于cookie的票據
.AutoDetect——如果device profile不支持cookies,就不使用基于cookie的票據;如果device profile支持cookies,那么就運用一種探測機制來判斷是否可以使用cookies.
.UseDeviceProfile——這是默認值.如果device profile支持cookies,就使用基于cookie的票據.不運用探測機制.
其中,AutoDetect 和 UseDeviceProfile選項都依靠一個device profile來判斷是使用基于cookie還是無cookie的票據.ASP.NET有一個關于這種devices及其性能的數據庫,比如某種devices是否支持cookies,它支持那個版本的JavaScript等信息.每次,當一個device向服務器發出對某個頁面的請求時,該請求里將包含一
個名為“user-agent”的HTTP header,用于表明device的類型.ASP.NET將自動的把提供的user-agent字符串與數據庫里相應的信息匹配起來.
注意:
該數據庫存儲在很多的XML文件里.這些默認的device profile文件,其路徑為%WINDIR%\Microsoft.Net\Framework\v2.0.50727\CONFIG\Browsers. 你也可以在你應用程序的App_Browsers文件夾里添加自定義的文件,關于這方面的更多信息,請參閱文章《How To: Detect Browser Types in ASP.NET Web Pages》
由于默認使用的是UseDeviceProfile選項.當訪問站點的某個device不支持cookies時,就站點就使用無cookie的票據.
在URL里對票據進行編碼
當瀏覽器每次向某個站點發出請求時,用來存儲信息的載體通常是Cookies.但如果訪問站點的device不支持Cookies的話,我們必須使用某種方法在客戶端和服務器端傳遞票據,通常的做法是在URL里將cookie數據編碼.
為了進行演示,我們將強迫站點使用無cookie的票據,為此我們將采用UseUri:
<authentication mode="Forms">
<forms cookieless="UseUri" slidingExpiration="true" timeout="60"/>
</authentication>
做了上述修改后,通過瀏覽器訪問.當以匿名用戶進行訪問時,URL看起來和以前沒什么區別,比如訪問Default.aspx頁面時,地址欄看起來和下面的差不多:
http://localhost:2448/ASPNET_Security_Tutorial_03_CS/default.aspx
然而一旦你登錄后,票據將加密到URL里.比如,當以Sam的名義登錄后,轉到Default.aspx頁面,這次地址欄看起來和下面的差不多:
該票據已經被編碼進URL。字符串(F(jaIOIDTJxIr12xYS-VVgkqKCVAuIoW30Bu0diWi6flQC-FyMaLXJfow_Vd9GZkB2Cv-rfezq0gKadKX0YPZCkA2)就是以16進制對票據信息編碼后的效果.這于通常情況下存儲在一個cookie里的數據是一樣的.
為使無cookie的票據工作正常,系統必須對所有頁面的URL編碼以包含票據數據.另外,當用戶點擊一個鏈接時,票據也不會丟失.還好,該編碼過程是自動執行的.為演示該功能,我們打開Default.aspx頁面,添加一個HyperLink鏈接,分別設置其Text 和 NavigateUrl屬性為“Test Link”和“SomePage.aspx”, 當然,這并不是說我們的站點真的有個頁面叫SomePage.aspx.
保存對Default.aspx的改動,再從瀏覽器訪問它.先登錄,以便把票據編碼進URL,然后,在Default.aspx頁面,點擊“Test Link”鏈接.會發生什么?如果不存在SomePage.aspx頁面,就會發生一個404錯誤,不過在這里這并不重要,注意觀察地址欄,在URL里包含了該票據!
鏈接里的URL——“SomePage.aspx”,將自動的添加到一個包含票據的一個URL里——我們不用手寫一行代碼!票據將自動的編碼進一個URL,只是該URL不能以 “http://”或“/”開頭. 至于該鏈接是出現在一個對Response.Redirect的調用,或一個HyperLink控件里,又或是一個anchor HTML元素(比如<a href="...">...</a>)里,那到無關緊要.只要URL不是“http://www.someserver.com/SomePage.aspx” 或 “/SomePage.aspx”這樣的形式,系統都會為我們自動編碼.
注意:
雖然和基于cookie的票據一樣,無cookie的票據也采取了某種過期策略,但無cookie的票據卻更用戶受到replay攻擊,原因正是因為票據被編碼進了URL.我們來設想一下,加入某個用戶訪問站點,成功登錄后將URL復制下來通過電郵傳給他的同事,在票據未過期之前,他的同事點擊該鏈接,那么他們都能以那個發郵件的用戶的名義登錄系統了!
第三步:Securing the Authentication Ticket
除了身份信息外,票據還可以包含用戶數據(我們將在第四步看到),因此將票據進行加密很有必要,更重要的是,forms authentication system必須保證票據未被篡改過.
為確保票據的真實性,forms authentication system必須驗證票據.驗證就是這樣的行為:確保某個數據塊沒有被修改過,這是通過message authentication code (MAC)來實現的. MAC就是一小片信息,用來對需要進行驗證的數據(就本文而言,就是票據)實施鑒別.如果數據被改動過,那么MAC就不能與改動過的數據匹配.另外,對某個黑客來說,既要改動數據,還要計算產生一個自己的MAC,以便使該MAC與改動過的數據匹配實在是太困難了.
當forms authentication system創建或改動一個票據后,就生成一個與票據數據相對應的MAC.當后續的請求到達時,forms authentication system就將MAC與票據數據進行比較,以驗證票據數據的真實性.圖3以圖表的形式對該工作流程進行了闡述.
對票據運用何種安全措施取決于<forms>元素里protection的配置.該項的值可為如下幾個值之一:
.All——默認值,票據要加密且運用數據有效性驗證
.Encryption——只加密,不生成MAC
.None——既不加密也不運用數據有效性驗證.
.Validation——生成一個MAC,但不對票據加密,以純文本的形式傳遞.
微軟強烈推薦使用All配置選項.
Setting the Validation and Decryption Keys
forms authentication system運用的加密方法以及驗證票據的散列法可以在Web.config文件的<machineKey>元素里進行用戶定制.Table 2列出了<machineKey>元素的屬性以及可能的取值.
對這些encryption 和 validation選項的深入研究,以及各種算法的優缺點探討已經超出了本系列文章的范疇,對這些問題的深入探討,包括使用哪種encryption 和 validation算法,密匙的長度,如何最好的生成這些keys,請參閱《Professional ASP.NET 2.0 Security, Membership, and Role Management》.
默認情況下,會自動為每個應用程序生成用于encryption和validation的keys,這些keys都存儲在Local Security Authority (LSA). 簡而言之,根據server-by-web server 和 application-by-application準則,默認配置保證keys都是唯一的、獨特的.因此,在這種默認行為在如下2種情況下是不行的:
.Web Farms——在一個web farm里,一個應用程序運行在多個服務器上,每個后續的請求都會指派到場中的一個服務器處理,這就意味著在一個用戶的會話期間,每個服務器都可能要處理他的請求.因此,每個服務器都必須使用相同的encryption 和 validation keys,這樣,在一臺服務器上created, encrypted, 以及validated的票據才能在另一臺服務器上進行decrypted和validated操作.
.Cross Application Ticket Sharing——一個服務器上可能運行了多個ASP.NET運用程序.如果你需要讓不同的應用程序之間共用一個單獨的票據,你必須使這些運用程序的encryption 和 validation keys匹配.
當你處于上述2種情況之一時,你必須在受影響的應用程序里配置<machineKey>元素,保證這些程序的decryptionKey 和 validationKey相互匹配.
即使你的應用程序沒有處于上述2種情況下,你也可以顯式的指定decryptionKey 和 validationKey值,以及定義要運用的運算法則.在Web.config文件里添加一個<machineKey>配置項:
<configuration>
<system.web> ... Some markup was removed for brevity ...
<machineKey decryption="AES" validation="SHA1" decryptionKey="1513F567EE75F7FB5AC0AC4D79E1D9F25430E3E2F1BCDD3370BCFC4EFC97A541" validationKey="32CBA563F26041EE5B5FE9581076C40618DCC1218F5F447634EDE8624508A129" />
</system.web>
</configuration>
更多詳情,請參閱《How To: Configure MachineKey in ASP.NET 2.0》
注意:
上面運用的這些decryptionKey 和 validationKey值,采用的是Steve Gibson的《Perfect Passwords web page》,詳情請參閱該文.
第四步:在票據里存儲附加的用戶數據
很多應用程序顯示當前登錄用戶的某些信息,比如用戶名或最后一次登錄的時間.票據存儲了當前用戶的名稱,但如果需要其它的信息時,頁面必須求助于用戶存儲——一般是某個數據庫——以查找需要的,但又沒有存儲在票據里的信息.
我們只需要很少的代碼就可以在票據里存儲附加的用戶信息.我們要用到FormsAuthenticationTicket類的UserData屬性.該屬性是存儲與用戶相關的數據量不大的信息的理想之地.對該屬性指定的值可以是authentication ticket cookie.根據forms authentication system的配置來進行加密和驗證.默認情況下,該UserData是一個空字符串.
為了在票據里存儲用戶數據,我們需要在登錄頁面寫代碼以獲取用戶信息并存儲進票據.由于UserData是一個字符串類型的屬性,因此存儲在該屬性里的數據必須要序列化為一個字符串.比如,假定我們的用戶存儲包括每個用戶的出生日期和所在公司名稱,我們希望將這2個屬性存儲在票據里.我們可以用(“|”)來將這2個屬性連接起來,比如某個用戶出生于August 15, 1974,所在公司為Northwind Traders,那么我們可以對UserData屬性賦值為這樣的字符串:“1974-08-15|Northwind Traders”.
任何時候當我們需要訪問存儲在票據里的數據時,我們可以獲取當前請求的FormsAuthenticationTicket,并對UserData屬性進行反序列化(deserializing).以上面的例子來說,我們可以根據分隔符(“|”)將UserData字符串分割為2個子字符串.
將信息寫入UserData
不幸的是,將用戶信息寫入票據并不如大家期望的那么簡單.FormsAuthenticationTicket類的UserData屬性是只讀的,只能通過FormsAuthenticationTicket類的構造器進行指定.當我們在構造器里為該屬性指定值時,我們還需要提供票據的其它值,比如:username,issue date, expiration等等.在前面的文章里,當我們創建登錄頁面時,FormsAuthentication類以及自動的幫我們進行了處理.要在票據里添加用戶數據時,我們要重新用到FormsAuthentication類里提供的、已有的函數.
讓我們對Login.aspx頁面進行更新,以向票據添加額外的信息,這樣來探究處理UserData所必需的代碼.假設我們的用戶存儲包含用戶公司的名稱,以及用戶的頭銜,而且我們想在票據里獲取這些信息.為此,對Login.aspx頁面的LoginButton Click事件處理器進行更新,如下:
protected void LoginButton_Click(object sender, EventArgs e)
{
// Three valid username/password pairs: Scott/password, Jisun/password, and Sam/password.
string[] users = { "Scott", "Jisun", "Sam" };
string[] passwords = { "password", "password", "password" };
string[] companyName = { "Northwind Traders", "Adventure Works", "Contoso" };
string[] titleAtCompany = { "Janitor", "Scientist", "Mascot" };
for (int i = 0; i < users.Length; i++)
{
bool validUsername = (string.Compare(UerName.Text, users[i], true) == 0);
bool validPassword = (string.Compare(Password.Text, passwords[i], false) == 0);
if (validUsername && validPassword)
{
// Query the user store to get this user's User Data
string userDataString = string.Concat(companyName[i], "|", titleAtCompany[i]);
// Create the cookie that contains the forms authentication ticket
HttpCookie authCookie = FormsAuthentication.GetAuthCookie(UserName.Text, RememberMe.Checked);
// Get the FormsAuthenticationTicket out of the encrypted cookie
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);
// Create a new FormsAuthenticationTicket that includes our custom User Data
FormsAuthenticationTicket newTicket = new FormsAuthenticationTicket(ticket.Version, ticket.Name, ticket.IssueDate, ticket.Expiration, ticket.IsPersistent, userDataString);
// Update the authCookie's Value to use the encrypted version of newTicket
authCookie.Value = FormsAuthentication.Encrypt(newTicket);
// Manually add the authCookie to the Cookies collection
Response.Cookies.Add(authCookie);
// Determine redirect URL and send user there
string redirUrl = FormsAuthentication.GetRedirectUrl(UserName.Text, RememberMe.Checked);
Response.Redirect(redirUrl);
}
}
// If we reach here, the user's credentials were invalid
InvalidCredentialsMessage.Visible = true;
}
我們一行一行的進行分析.該方法首先定義了4個字符串數組:users, passwords, companyName,和titleAtCompany.這些數組用于存儲用戶名、密碼、公司名稱、以及用戶頭銜.這里有3個用戶Scott, Jisun,Sam.在實際的應用程序里,這些值都是從用戶存儲里查詢得到的,不像我們這樣在頁面的源代碼里"硬編碼"得到的.
在前面的文章里,如果用戶提供的登錄信息有誤,我們僅僅調用FormsAuthentication.RedirectFromLoginPage(UserName.Text, RememberMe.Checked)方法,它執行如下的步驟:
1.創建表單驗證票據
2.將票據寫入適當的存儲.對基于cookies的票據而言,使用的是瀏覽器的cookies collection;而對無cookies的票據而言,將票據數據序列化進URL
3.將用戶重導航到某個恰當的頁面
這些步驟在上述代碼里都實現了.首先,我們用(“|”)將company name和title連接起來,作為存儲到UserData屬性里的最終字符串,如下:
string userDataString = string.Concat(companyName[i], "|", titleAtCompany[i]);
接下來,調用FormsAuthentication.GetAuthCookie方法,它創建票據,根據配置的情況進行encrypts 和 validates處理,并賦值給一個HttpCookie對象:
HttpCookie authCookie = FormsAuthentication.GetAuthCookie(UserName.Text, RememberMe.Checked);
為了處理植入cookie里的FormAuthenticationTicket,我們需要調用FormAuthentication類的Decrypt方法, 傳入cookie值,如下:
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);
然后我們在現有FormsAuthenticationTicket的值的基礎上,創建一個新的FormsAuthenticationTicket實例,該新票據包含與用戶有關的信息(也就是userDataString),如下:
FormsAuthenticationTicket newTicket = new FormsAuthenticationTicket(ticket.Version, ticket.Name, ticket.IssueDate, ticket.Expiration, ticket.IsPersistent, userDataString);
當調用Encrypt方法對新票據進行encrypt (以及validate)處理后,又將其返回給authCookie,如下:
authCookie.Value = FormsAuthentication.Encrypt(newTicket);
最后將authCookie添加到Response.Cookies集合,并調用GetRedirectUrl方法將用戶導航到恰當的頁面.
Response.Cookies.Add(authCookie);
string redirUrl = FormsAuthentication.GetRedirectUrl(UserName.Text, RememberMe.Checked);
Response.Redirect(redirUrl);
所有這些代碼都是必須的,因為UserData屬性是只讀的,且在FormsAuthentication類的GetAuthCookie, SetAuthCookie,或 RedirectFromLoginPage方法里都沒有提供任何指定UserData信息的途徑.
注意:
以上代碼將用戶相關的信息存儲進基于cookies的票據.另一方面,將票據序列化到URL的工作是由.NET Framework內部的類進行處理的,因此,對無cookie的票據而言,我們無法將用戶數據存儲到票據里.
訪問UserData信息
至此,當用戶登錄時,用戶的公司名以及他們的頭銜都存儲在票據的UserData屬性里.在任何一個頁面上,不用再去查詢用戶存儲,我們就可以通過票據訪問這些信息.為進行演示,我們對Default.aspx頁面進行更新,使歡迎信息里不但包含用戶名,還包含公司名和他的頭銜.
目前,Default.aspx頁面包含一個名為AuthenticatedMessagePanel的Panel控件,里面有一個名為WelcomeBackMessage的Label控件.該Panel是面向認證用戶的.對Default.aspx頁面的Page_Load事件處理器進行更新,如下:
protected void Page_Load(object sender, EventArgs e)
{
if (Request.IsAuthenticated)
{ WelcomeBackMessage.Text = "Welcome back, " + User.Identity.Name + "!";
// Get User Data from FormsAuthenticationTicket and show it in WelcomeBackMessage
FormsIdentity ident = User.Identity as FormsIdentity;
if (ident != null)
{ FormsAuthenticationTicket ticket = ident.Ticket;
string userDataString = ticket.UserData;
// Split on the |
string[] userDataPieces = userDataString.Split("|".ToCharArray());
string companyName = userDataPieces[0];
string titleAtCompany = userDataPieces[1];
WelcomeBackMessage.Text += string.Format(" You are the {0} of {1}.", titleAtCompany, companyName);
}
AuthenticatedMessagePanel.Visible = true;
AnonymousMessagePanel.Visible = false; }
else
{ AuthenticatedMessagePanel.Visible = false;
AnonymousMessagePanel.Visible = true; }
}
如果Request.IsAuthenticated為true,則將WelcomeBackMessage的Text屬性設置為“Welcome back, username.” ,然后將User.Identity屬性轉換為一個FormsIdentity對象,以便于我們訪問潛在的FormsAuthenticationTicket.一旦我們獲取到FormsAuthenticationTicket后,我們根據分割符"|",將UserData屬性反序列化為2個代表公司和頭銜的字符串.然后將公司名和頭銜顯示在WelcomeBackMessage Label里.
圖5顯示的是實際的一個截屏,以Scott的名義登錄,歡迎消息里包含Scott的公司名和頭銜.
圖5:
注意:
票據的UserData屬性是用戶存儲的一個緩存.與其它任何緩存一樣,當“源數據”發生改動時,需要對緩存進行刷新.
第五步:Using a Custom Principal
每次請求抵達時,FormsAuthenticationModule都會對用戶進行鑒別.如果票據未過期,FormsAuthenticationModule就將HttpContext.User屬性賦值為一個新的 該GenericPrincipal對象有一個代表表單認證票據FormsIdentity.而GenericPrincipal類包含的功能僅僅夠貫徹IPrincipal接口——它只有一個Identity屬性和一個IsInRole方法.
principal對象有2個職責:指出用戶屬于什么角色以及提供identity信息.這2個職責是分別通過IPrincipal接口的IsInRole(roleName)方法和Identity屬性來實現的.GenericPrincipal類允許通過它的構造器為role名稱指定一個字符串數組,而其IsInRole(roleName)方法僅僅檢查傳入的roleName是否存在于該字符串數組里.當FormsAuthenticationModule創建GenericPrincipal時,在GenericPrincipal的構造器里傳入一個空的字符串數組,因此當調用IsInRole時,總是返回false.
對絕大多數沒有用到role的、基于表單的認證而言,該GenericPrincipal類是完全滿足需要的.如果這種對role的默認處理無法滿足你的需要,或者你需要將用戶與一個自定義IIdentity對象聯系起來的話,你可以在認證流程里創建一個自定義的IPrincipal對象,并將其賦值給HttpContext.User屬性.
注意:
正如你將在以后的文章看到的那樣,當使ASP.NET’s Roles framework創建一個類型為RolePrincipal的自定義principal對象,并重寫forms authentication創建GenericPrincipal對象的方法時,我們必須這樣做,對principal的IsInRole方法進行定制,以便與Roles framework的API“接軌”.
由于我們現在還沒有牽涉到role,因此本文只探討自定義IIdentity對象的問題.在第四步,我們考察了將附加的用戶信息存儲在票據的UserData屬性里,具體來說就是公司名和員工的頭銜.然而,UserData信息只能通過票據進行訪問,且是一個連續的字符串,這就意味著我們想訪問存儲在票據里的用戶信息時,必須對UserData屬性進行解析.
你還可以創建一個貫徹IIdentity接口的類,使它包含CompanyName屬性和Title屬性.那樣的話,開發人員就可以直接通過CompanyName 和 Title屬性直接訪問當前登錄用戶的公司名稱和頭銜,而不用知道如何對UserData屬性進行解析了.
創建自定義Identity 和 Principal類
本文,我們來在App_Code文件夾里創建自定義的principal和identity對象.首先在項目里添加一個App_Code文件夾,該文件夾專門用于存放專用于本站點的class文件.
注意:
只有當通過Website Project Model對項目進行管理時才應使用App_Code文件夾.如果你使用的是Web Application Project Model, 你只需要創建一個一般的文件夾,將class文件放進去即可.比如,你創建一個名為Classes的文件夾,以存放你的類.然后,向App_Code文件夾里添加2個class文件,一個叫CustomIdentity.cs,另一個叫CustomPrincipal.cs.
圖6:向你的項目添加CustomIdentity 和 CustomPrincipal類
該CustomIdentity類用于執行IIdentity接口,而IIdentity接口又定義了AuthenticationType, IsAuthenticated,以及Name屬性.除了這些必需的屬性外,我們還對表示票據里的用戶公司名和頭銜的屬性感興趣.在CustomIdentity類里鍵入如下的代碼:
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public class CustomIdentity : System.Security.Principal.IIdentity
{
private FormsAuthenticationTicket _ticket;
public CustomIdentity(FormsAuthenticationTicket ticket)
{
_ticket = ticket;
}
public string AuthenticationType
{
get { return "Custom"; }
}
public bool IsAuthenticated
{
get { return true; }
}
public string Name
{
get { return _ticket.Name; }
}
public FormsAuthenticationTicket Ticket
{
get { return _ticket; }
}
public string CompanyName
{
get {
string[] userDataPieces = _ticket.UserData.Split("|".ToCharArray());
return userDataPieces[0]; }
}
public string Title
{
get
{
string[] userDataPieces = _ticket.UserData.Split("|".ToCharArray());
return userDataPieces[1];
}
}
}
注意該類里包含一個FormsAuthenticationTicket類型的成員變量(_ticket),通過構造器來提供票據信息.票據數據用于返回identity的Name;其UserData屬性經過解析以返回CompanyName 和 Title屬性的值.
接下來創建CustomPrincipal類,因為在這里我們暫時不關心role的問題,因此該CustomPrincipal類的構造器僅僅接受一個CustomIdentity對象,其IsInRole方法總是返回false,如下:
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public class CustomPrincipal : System.Security.Principal.IPrincipal
{
private CustomIdentity _identity;
public CustomPrincipal(CustomIdentity identity)
{
_identity = identity;
}
public System.Security.Principal.IIdentity Identity
{
get { return _identity; }
}
public bool IsInRole(string role)
{
return false; }
}
將一個CustomPrincipal對象賦值給隨后請求的安全內容
到目前為止,我們有一個對默認的IIdentity接口進行擴展的類,它還包含CompanyName和Title屬性;也有一個使用我們自定義identity的自定義principal類.我們現在將深入ASP.NET,將我們自定義的principal對象賦值給隨后的請求的安全內容里.
ASP.NET接到一個請求后,通過一系列的步驟對請求進行處理.在每一步都會觸發一個具體的事件,這就為開發人員楔入ASP.NET處理通道內部,在其生命周期的某一點上對請求進行修改提供了可能.以FormsAuthenticationModule為例,當ASP.NET引發AuthenticateReques事件后,在該事件里它對請求進行認證票據檢查,如果在請求里發現了票據,就生成一個GenericPrincipal對象,并賦值給HttpContext.User屬性.
在AuthenticateRequest事件之后,ASP.NET又引發PostAuthenticateRequest事件, 在該事件里,我們可以將FormsAuthenticationModule創建的GenericPrincipal對象替換為我們自定義的CustomPrincipal對象的一個實例,圖7描繪了該流程.
為了對ASP.NET的這些事件做出回應,我們要么在Global.asax文件里創建相應的事件處理器,要么創建我們自己的HTTP Module,就本系列文章而言,我們在Global.asax文件里創建事件處理器.首先將Global.asax文件添加到站點.
默認的Global.asax模板里包含了對應于一些ASP.NET事件的事件處理器,包括Start, End以及Error事件等.放心大膽的將這些事件處理器刪除,因為我們的應用程序里不需要用到它們,我們關心的事件是PostAuthenticateRequest.對你的Global.asax文件進行更新,使其代碼看起來和下面的差不多:
<%@ Application Language="C#" %>
<%@ Import Namespace="System.Security.Principal" %>
<%@ Import Namespace="System.Threading" %>
<script runat="server">
void Application_OnPostAuthenticateRequest(object sender, EventArgs e)
{
// Get a reference to the current User
IPrincipal usr = HttpContext.Current.User;
// If we are dealing with an authenticated forms authentication request
if (usr.Identity.IsAuthenticated && usr.Identity.AuthenticationType == "Forms") {
FormsIdentity fIdent = usr.Identity as FormsIdentity;
// Create a CustomIdentity based on the FormsAuthenticationTicket CustomIdentity ci = new
CustomIdentity(fIdent.Ticket);
// Create the CustomPrincipal
CustomPrincipal p = new CustomPrincipal(ci);
// Attach the CustomPrincipal to HttpContext.User and Thread.CurrentPrincipal HttpContext.Current.User = p;
Thread.CurrentPrincipal = p;
}
}
</script>
該Application_OnPostAuthenticateRequest方法只在ASP.NET runtime引發PostAuthenticateRequest事件時才執行,且在每個請求抵達時只發生一次.代碼首先檢查用戶是否通過了認證,且認證方式為表單認證.如果是的,則創建一個新的CustomIdentity對象,并將當前請求的票據傳入其構造器.接下來,創建一個CustomPrincipal對象,并將剛才創建的CustomIdentity對象傳到其構造器里.最后,將最新創建的CustomPrincipal對象賦值給當前請求的安全內容.
注意最后一步——將principal分配給HttpContext.User和Thread.CurrentPrincipal.這2步是必需的,因為是ASP.NET來處理的安全內容.我們知道,.NET Framework將一個安全內容與每個running thread聯系起來;我們可以通過Thread object的CurrentPrincipal屬性,以IPrincipal對象的形式來獲得這些信息. 容易讓人混淆的是ASP.NET也有自己的安全內容信息(也就是HttpContext.User)
在某些情況下通過檢查Thread.CurrentPrincipal屬性來判定安全內容;而在另一些情況下是通過檢查HttpContext.User屬性來實現的.比如,在.NET里有一些安全特性(security features),利用這些特性來聲明哪些用戶或角色可以使用一個類或調用某個特定的方法(請參閱《Adding Authorization Rules to Business and Data Layers Using PrincipalPermissionAttributes),此外,這些技術還可以通過Thread.CurrentPrincipal屬性來判定安全內容.
我們再看看使用HttpContext.User用戶的情形.再前面的文章里,我們使用該屬性來顯示當前登陸用戶的用戶名.自然的,Thread.CurrentPrincipal屬性和HttpContext.User屬性包含的安全內容必須匹配.
ASP.NET runtime自動的為我們同步的處理這些屬性.然而,該同步是發生在AuthenticateRequest事件之后,PostAuthenticateRequest事件之前。因此,當在PostAuthenticateRequest事件里添加一個自定義principal的時候,我們需要手動地為Thread.CurrentPrincipal賦值,不然Thread.CurrentPrincipal 和 HttpContext.User就不同步了. 更多細節請參閱《Context.User vs. Thread.CurrentPrincipal》.
訪問CompanyName 和 Title屬性
任何時候,當請求抵達并由ASP.NET引擎分派時,都會引發Global.asax文件里的Application_OnPostAuthenticateRequest事件,如果請求成功通過FormsAuthenticationModule的認證的話,該事件處理器將創建一個新的CustomPrincipal對象,該對象將包含一個基于票據的CustomIdentity對象.在這里進行這些邏輯處理后,我們就可以相當方便地訪問當前用戶的公司名和頭銜.
返回到Default.aspx頁面的Page_Load事件處理器,在第四步,我們在該事件處理器里寫代碼檢索票據,并將UserData數據進行解析以顯示公司名和頭銜.現在使用CustomPrincipal 和 CustomIdentity對象后,我們就不要對票據的UserData屬性進行解析了. 僅僅需要獲取對CustomIdentity對象的引用,并使用其CompanyName和Title屬性,如下:
CustomIdentity ident = User.Identity as CustomIdentity;
if (ident != null) WelcomeBackMessage.Text += string.Format(" You are the {0} of {1}.", ident.Title, ident.CompanyName);
結語:
在本文,我們考察了如何通過Web.config文件來對表單認證系統進行配置.我們也考察了如何處理票據的有效期,以及encryption和validation安全措施是如何從inspection和modification這2方面對票據進行保護的.最后,我們探討了使用票據的UserData屬性來存儲票據的附加信息.,以及如何使用自定義的principal和identity對象來以一種更好的方式訪問這些附加信息.
到本文為止,我們考察完了ASP.NET里的表單認證.下一篇文章我們將開始考察Membership framework.
祝編程快樂!
Security Tutorials系列文章第三章:Forms Authentication Configuration and Advanced Topics
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
