閱讀目錄
用戶(hù)登錄是個(gè)很常見(jiàn)的業(yè)務(wù)需求,在A(yíng)SP.NET中,這個(gè)過(guò)程被稱(chēng)為身份認證。由于很常見(jiàn),因此,我認為把這塊內容整理出來(lái),與大家分享應該是件有意義的事。
在開(kāi)發(fā)ASP.NET項目中,我們最常用的是Forms認證,也叫【表單認證】。這種認證方式既可以用于局域網(wǎng)環(huán)境,也可用于互聯(lián)網(wǎng)環(huán)境,因此,它有著(zhù)非常廣泛的使用。這篇博客主要討論的話(huà)題是:ASP.NET Forms 身份認證。
有一點(diǎn)我要申明一下:在這篇博客中,不會(huì )涉及ASP.NET的登錄系列控件以及membership的相關(guān)話(huà)題,我只想用比較原始的方式來(lái)說(shuō)明在A(yíng)SP.NET中是如何實(shí)現身份認證的過(guò)程。
在開(kāi)始今天的博客之前,我想有二個(gè)最基礎的問(wèn)題首先要明確:
1. 如何判斷當前請求是一個(gè)已登錄用戶(hù)發(fā)起的?
2. 如何獲取當前登錄用戶(hù)的登錄名?
在標準的ASP.NET身份認證方式中,上面二個(gè)問(wèn)題的答案是:
1. 如果Request.IsAuthenticated為true,則表示是一個(gè)已登錄用戶(hù)。
2. 如果是一個(gè)已登錄用戶(hù),訪(fǎng)問(wèn)HttpContext.User.Identity.Name可獲取登錄名(都是實(shí)例屬性)。
接下來(lái),本文將會(huì )圍繞上面二個(gè)問(wèn)題展開(kāi),請繼續閱讀。
在A(yíng)SP.NET中,整個(gè)身份認證的過(guò)程其實(shí)可分為二個(gè)階段:認證與授權。
1. 認證階段:識別當前請求的用戶(hù)是不是一個(gè)可識別(的已登錄)用戶(hù)。
2. 授權階段:是否允許當前請求訪(fǎng)問(wèn)指定的資源。
這二個(gè)階段在A(yíng)SP.NET管線(xiàn)中用AuthenticateRequest和AuthorizeRequest事件來(lái)表示。
在認證階段,ASP.NET會(huì )檢查當前請求,根據web.config設置的認證方式,嘗試構造HttpContext.User對象供我們在后續的處理中使用。在授權階段,會(huì )檢查當前請求所訪(fǎng)問(wèn)的資源是否允許訪(fǎng)問(wèn),因為有些受保護的頁(yè)面資源可能要求特定的用戶(hù)或者用戶(hù)組才能訪(fǎng)問(wèn)。所以,即使是一個(gè)已登錄用戶(hù),也有可能會(huì )不能訪(fǎng)問(wèn)某些頁(yè)面。當發(fā)現用戶(hù)不能訪(fǎng)問(wèn)某個(gè)頁(yè)面資源時(shí),ASP.NET會(huì )將請求重定向到登錄頁(yè)面。
受保護的頁(yè)面與登錄頁(yè)面我們都可以在web.config中指定,具體方法可參考后文。
在A(yíng)SP.NET中,Forms認證是由FormsAuthenticationModule實(shí)現的,URL的授權檢查是由UrlAuthorizationModule實(shí)現的。
前面我介紹了可以使用Request.IsAuthenticated來(lái)判斷當前用戶(hù)是不是一個(gè)已登錄用戶(hù),那么這一過(guò)程又是如何實(shí)現的呢?
為了回答這個(gè)問(wèn)題,我準備了一個(gè)簡(jiǎn)單的示例頁(yè)面,代碼如下:
<fieldset><legend>用戶(hù)狀態(tài)</legend><form action="<%= Request.RawUrl %>" method="post"> <% if( Request.IsAuthenticated ) { %> 當前用戶(hù)已登錄,登錄名:<%= Context.User.Identity.Name.HtmlEncode() %> <br /> <input type="submit" name="Logon" value="退出" /> <% } else { %> <b>當前用戶(hù)還未登錄。</b> <% } %> </form></fieldset>
頁(yè)面顯示效果如下:

根據前面的代碼,我想現在能看到這個(gè)頁(yè)面顯示也是正確的,是的,我目前還沒(méi)有登錄(根本還沒(méi)有實(shí)現這個(gè)功能)。
下面我再加點(diǎn)代碼來(lái)實(shí)現用戶(hù)登錄。頁(yè)面代碼:
<fieldset><legend>普通登錄</legend><form action="<%= Request.RawUrl %>" method="post"> 登錄名:<input type="text" name="loginName" style="width: 200px" value="Fish" /> <input type="submit" name="NormalLogin" value="登錄" /></form></fieldset>
現在頁(yè)面的顯示效果:

登錄與退出登錄的實(shí)現代碼:
public void Logon(){ FormsAuthentication.SignOut();}public void NormalLogin(){ // ----------------------------------------------------------------- // 注意:演示代碼為了簡(jiǎn)單,這里不檢查用戶(hù)名與密碼是否正確。 // ----------------------------------------------------------------- string loginName = Request.Form["loginName"]; if( string.IsNullOrEmpty(loginName) ) return; FormsAuthentication.SetAuthCookie(loginName, true); TryRedirect();}
現在,我可試一下登錄功能。點(diǎn)擊登錄按鈕后,頁(yè)面的顯示效果如下:

從圖片的顯示可以看出,我前面寫(xiě)的NormalLogin()方法確實(shí)可以實(shí)現用戶(hù)登錄。
當然了,我也可以在此時(shí)點(diǎn)擊退出按鈕,那么就回到了圖片2的顯示。
寫(xiě)到這里,我想有必要再來(lái)總結一下在A(yíng)SP.NET中實(shí)現登錄與注銷(xiāo)的方法:
1. 登錄:調用FormsAuthentication.SetAuthCookie()方法,傳遞一個(gè)登錄名即可。
2. 注銷(xiāo):調用FormsAuthentication.SignOut()方法。
在一個(gè)ASP.NET網(wǎng)站中,有些頁(yè)面會(huì )允許所有用戶(hù)訪(fǎng)問(wèn),包括一些未登錄用戶(hù),但有些頁(yè)面則必須是已登錄用戶(hù)才能訪(fǎng)問(wèn),還有一些頁(yè)面可能會(huì )要求特定的用戶(hù)或者用戶(hù)組的成員才能訪(fǎng)問(wèn)。這類(lèi)頁(yè)面因此也可稱(chēng)為【受限頁(yè)面】,它們一般代表著(zhù)比較重要的頁(yè)面,包含一些重要的操作或功能。
為了保護受限制的頁(yè)面的訪(fǎng)問(wèn),ASP.NET提供了一種簡(jiǎn)單的方式:可以在web.config中指定受限資源允許哪些用戶(hù)或者用戶(hù)組(角色)的訪(fǎng)問(wèn),也可以設置為禁止訪(fǎng)問(wèn)。
比如,網(wǎng)站有一個(gè)頁(yè)面:MyInfo.aspx,它要求訪(fǎng)問(wèn)這個(gè)頁(yè)面的訪(fǎng)問(wèn)者必須是一個(gè)已登錄用戶(hù),那么可以在web.config中這樣配置:
<location path="MyInfo.aspx"> <system.web> <authorization> <deny users="?"/> </authorization> </system.web></location>
為了方便,我可能會(huì )將一些管理相關(guān)的多個(gè)頁(yè)面放在A(yíng)dmin目錄中,顯然這些頁(yè)面只允許Admin用戶(hù)組的成員才可以訪(fǎng)問(wèn)。對于這種情況,我們可以直接針對一個(gè)目錄設置訪(fǎng)問(wèn)規則:
<location path="Admin"> <system.web> <authorization> <allow roles="Admin"/> <deny users="*"/> </authorization> </system.web></location>
這樣就不必一個(gè)一個(gè)頁(yè)面單獨設置了,還可以在目錄中創(chuàng )建一個(gè)web.config來(lái)指定目錄的訪(fǎng)問(wèn)規則,請參考后面的示例。
在前面的示例中,有一點(diǎn)要特別注意的是:
1. allow和deny之間的順序一定不能寫(xiě)錯了,UrlAuthorizationModule將按這個(gè)順序依次判斷。
2. 如果某個(gè)資源只允許某類(lèi)用戶(hù)訪(fǎng)問(wèn),那么最后的一條規則一定是 <deny users="*" />
在allow和deny的配置中,我們可以在一條規則中指定多個(gè)用戶(hù):
1. 使用users屬性,值為逗號分隔的用戶(hù)名列表。
2. 使用roles屬性,值為逗號分隔的角色列表。
3. 問(wèn)號 (?) 表示匿名用戶(hù)。
4. 星號 (*) 表示所有用戶(hù)。
有時(shí)候,我們可能要開(kāi)發(fā)一個(gè)內部使用的網(wǎng)站程序,這類(lèi)網(wǎng)站程序要求 禁止匿名用戶(hù)的訪(fǎng)問(wèn),即:所有使用者必須先登錄才能訪(fǎng)問(wèn)。因此,我們通常會(huì )在網(wǎng)站根目錄下的web.config中這樣設置:
<authorization> <deny users="?"/></authorization>
對于我們的示例,我們也可以這樣設置。此時(shí)在瀏覽器打開(kāi)頁(yè)面時(shí),呈現效果如下:

從圖片中可以看出:頁(yè)面的樣式顯示不正確,最下邊還多出了一行文字。
這個(gè)頁(yè)面的完整代碼是這樣的(它引用了一個(gè)CSS文件和一個(gè)JS文件):
<%@ Page Language="C#" CodeFile="Default.aspx.cs" Inherits="_Default" %><html xmlns="http://www.w3.org/1999/xhtml"><head> <title>FormsAuthentication DEMO - http://www.cnblogs.com/fish-li/</title> <link type="text/css" rel="Stylesheet" href="css/StyleSheet.css" /></head><body> <fieldset><legend>普通登錄</legend><form action="<%= Request.RawUrl %>" method="post"> 登錄名:<input type="text" name="loginName" style="width: 200px" value="Fish" /> <input type="submit" name="NormalLogin" value="登錄" /> </form></fieldset> <fieldset><legend>用戶(hù)狀態(tài)</legend><form action="<%= Request.RawUrl %>" method="post"> <% if( Request.IsAuthenticated ) { %> 當前用戶(hù)已登錄,登錄名:<%= Context.User.Identity.Name.HtmlEncode() %> <br /> <% var user = Context.User as MyFormsPrincipal<UserInfo>; %> <% if( user != null ) { %> <%= user.UserData.ToString().HtmlEncode() %> <% } %> <input type="submit" name="Logon" value="退出" /> <% } else { %> <b>當前用戶(hù)還未登錄。</b> <% } %> </form></fieldset> <p id="hideText"><i>不應該顯示的文字</i></p> <script type="text/javascript" src="js/JScript.js"></script></body></html>
頁(yè)面最后一行文字平時(shí)不顯示是因為JScript.js中有以下代碼:
document.getElementById("hideText").setAttribute("style", "display: none");
這段JS代碼能做什么,我想就不用再解釋了。
雖然這段JS代碼沒(méi)什么價(jià)值,但我主要是想演示在登錄頁(yè)面中引用JS的場(chǎng)景。
根據前面圖片,我們可以猜測到:應該是CSS和JS文件沒(méi)有正確加載造成的。
為了確認就是這樣原因,我們可以打開(kāi)FireBug再來(lái)看一下頁(yè)面加載情況:

根據FireBug提供的線(xiàn)索我們可以分析出,頁(yè)面在訪(fǎng)問(wèn)CSS, JS文件時(shí),其實(shí)是被重定向到登錄頁(yè)面了,因此獲得的結果肯定也是無(wú)意義的,所以就造成了登錄頁(yè)的顯示不正確。
還記得前所說(shuō)的【授權】嗎?
是的,現在就是由于我們在web.config中設置了不允許匿名用戶(hù)訪(fǎng)問(wèn),因此,所有的資源也就不允許匿名用戶(hù)訪(fǎng)問(wèn)了,包括登錄頁(yè)所引用的CSS, JS文件。當授權檢查失敗時(shí),請求會(huì )被重定向到登錄頁(yè)面,所以,登錄頁(yè)本身所引用的CSS, JS文件最后得到的響應內容其實(shí)是登錄頁(yè)的HTML代碼,最終導致它們不能發(fā)揮作用,表現為登錄頁(yè)的樣式顯示不正確,以及引用的JS文件也不起作用。
不過(guò),有一點(diǎn)比較奇怪:為什么訪(fǎng)問(wèn)登錄頁(yè)面時(shí),沒(méi)有發(fā)生重定向呢?
原因是這樣的:在A(yíng)SP.NET內部,當發(fā)現是在訪(fǎng)問(wèn)登錄面時(shí),會(huì )設置HttpContext.SkipAuthorization = true (其實(shí)是一個(gè)內部調用),這樣的設置會(huì )告訴后面的授權檢查模塊:跳過(guò)這次請求的授權檢查。因此,登錄頁(yè)總是允許所有用戶(hù)訪(fǎng)問(wèn),但是CSS文件以及JS文件是在另外的請求中發(fā)生的,那些請求并不會(huì )要跳過(guò)授權模塊的檢查。
為了解決登錄頁(yè)不能正確顯示的問(wèn)題,我們可以這樣處理:
1. 在網(wǎng)站根目錄中的web.config中設置登錄頁(yè)所引用的JS, CSS文件都允許匿名訪(fǎng)問(wèn)。
2. 也可以直接針對JS, CSS目錄設置為允許匿名用戶(hù)訪(fǎng)問(wèn)。
3. 還可以在CSS, JS目錄中創(chuàng )建一個(gè)web.config文件來(lái)配置對應目錄的授權規則??蓞⒖家韵聎eb.config文件:
<?xml version="1.0"?><configuration> <system.web> <authorization> <allow users="*"/> </authorization> </system.web></configuration>
第三種做法可以不修改網(wǎng)站根目錄下的web.config文件。
注意:在IIS中看到的情況就和在Visual Studio中看到的結果就不一樣了。因為,像js, css, image這類(lèi)文件屬于靜態(tài)資源文件,IIS能直接處理,不需要交給ASP.NET來(lái)響應,因此就不會(huì )發(fā)生授權檢查失敗,所以,如果這類(lèi)網(wǎng)站部署在IIS中,看到的結果又是正常的。
前面我演示了如何用代碼實(shí)現登錄與注銷(xiāo)的過(guò)程,下面再來(lái)看一下登錄時(shí),ASP.NET到底做了些什么事情,它是如何知道當前請求是一個(gè)已登錄用戶(hù)的?
在繼續探索這個(gè)問(wèn)題前,我想有必要來(lái)了解一下HTTP協(xié)議的一些特點(diǎn)。
HTTP是一個(gè)無(wú)狀態(tài)的協(xié)議,無(wú)狀態(tài)的意思可以理解為:WEB服務(wù)器在處理所有傳入請求時(shí),根本就不知道某個(gè)請求是否是一個(gè)用戶(hù)的第一次請求與后續請求,或者是另一個(gè)用戶(hù)的請求。WEB服務(wù)器每次在處理請求時(shí),都會(huì )按照用戶(hù)所訪(fǎng)問(wèn)的資源所對應的處理代碼,從頭到尾執行一遍,然后輸出響應內容,WEB服務(wù)器根本不會(huì )記住已處理了哪些用戶(hù)的請求,因此,我們通常說(shuō)HTTP協(xié)議是無(wú)狀態(tài)的。
雖然HTTP協(xié)議與WEB服務(wù)器是無(wú)狀態(tài),但我們的業(yè)務(wù)需求卻要求有狀態(tài),典型的就是用戶(hù)登錄,在這種業(yè)務(wù)需求中,要求WEB服務(wù)器端能區分某個(gè)請求是不是一個(gè)已登錄用戶(hù)發(fā)起的,或者當前請求是哪個(gè)用戶(hù)發(fā)出的。在開(kāi)發(fā)WEB應用程序時(shí),我們通常會(huì )使用Cookie來(lái)保存一些簡(jiǎn)單的數據供服務(wù)端維持必要的狀態(tài)。既然這是個(gè)通常的做法,那我們現在就來(lái)看一下現在頁(yè)面的Cookie使用情況吧,以下是我用FireFox所看到的Cookie列表:

這個(gè)名字:LoginCookieName,是我在web.config中指定的:
<authentication mode="Forms" > <forms cookieless="UseCookies" name="LoginCookieName" loginUrl="~/Default.aspx"></forms></authentication>
在這段配置中,我不僅指定的登錄狀態(tài)的Cookie名,還指定了身份驗證模式,以及Cookie的使用方式。
為了判斷這個(gè)Cookie是否與登錄狀態(tài)有關(guān),我們可以在瀏覽器提供的界面刪除它,然后刷新頁(yè)面,此時(shí)頁(yè)面的顯示效果如下:

此時(shí),頁(yè)面顯示當前用戶(hù)沒(méi)有登錄。
為了確認這個(gè)Cookie與登錄狀態(tài)有關(guān),我們可以重新登錄,然后再退出登錄。
發(fā)現只要是頁(yè)面顯示當前用戶(hù)未登錄時(shí),這個(gè)Cookie就不會(huì )存在。
事實(shí)上,通過(guò)SetAuthCookie這個(gè)方法名,我們也可以猜得出這個(gè)操作會(huì )寫(xiě)一個(gè)Cookie。
注意:本文不討論無(wú)Cookie模式的Forms登錄。
從前面的截圖我們可以看出:雖然當前用戶(hù)名是 Fish ,但是,Cookie的值是一串亂碼樣的字符串。
由于安全性的考慮,ASP.NET對Cookie做過(guò)加密處理了,這樣可以防止惡意用戶(hù)構造Cookie繞過(guò)登錄機制來(lái)模擬登錄用戶(hù)。如果想知道這串加密字符串是如何得到的,那么請參考后文。
小結:
1. Forms身份認證是在web.config中指定的,我們還可以設置Forms身份認證的其它配置參數。
2. Forms身份認證的登錄狀態(tài)是通過(guò)Cookie來(lái)維持的。
3. Forms身份認證的登錄Cookie是加密的。
經(jīng)過(guò)前面的Cookie分析,我們可以發(fā)現Cookie的值是一串加密后的字符串,現在我們就來(lái)分析這個(gè)加密過(guò)程以及Cookie對于身份認證的作用。
登錄的操作通常會(huì )檢查用戶(hù)提供的用戶(hù)名和密碼,因此登錄狀態(tài)也必須具有足夠高的安全性。在Forms身份認證中,由于登錄狀態(tài)是保存在Cookie中,而Cookie又會(huì )保存到客戶(hù)端,因此,為了保證登錄狀態(tài)不被惡意用戶(hù)偽造,ASP.NET采用了加密的方式保存登錄狀態(tài)。為了實(shí)現安全性,ASP.NET采用【Forms身份驗證憑據】(即FormsAuthenticationTicket對象)來(lái)表示一個(gè)Forms登錄用戶(hù),加密與解密由FormsAuthentication的Encrypt與Decrypt的方法來(lái)實(shí)現。
用戶(hù)登錄的過(guò)程大致是這樣的:
1. 檢查用戶(hù)提交的登錄名和密碼是否正確。
2. 根據登錄名創(chuàng )建一個(gè)FormsAuthenticationTicket對象。
3. 調用FormsAuthentication.Encrypt()加密。
4. 根據加密結果創(chuàng )建登錄Cookie,并寫(xiě)入Response。
在登錄驗證結束后,一般會(huì )產(chǎn)生重定向操作,那么后面的每次請求將帶上前面產(chǎn)生的加密Cookie,供服務(wù)器來(lái)驗證每次請求的登錄狀態(tài)。
每次請求時(shí)的(認證)處理過(guò)程如下:
1. FormsAuthenticationModule嘗試讀取登錄Cookie。
2. 從Cookie中解析出FormsAuthenticationTicket對象。過(guò)期的對象將被忽略。
3. 根據FormsAuthenticationTicket對象構造FormsIdentity對象并設置HttpContext.User
4. UrlAuthorizationModule執行授權檢查。
在登錄與認證的實(shí)現中,FormsAuthenticationTicket和FormsAuthentication是二個(gè)核心的類(lèi)型,前者可以認為是一個(gè)數據結構,后者可認為是處理前者的工具類(lèi)。
UrlAuthorizationModule是一個(gè)授權檢查模塊,其實(shí)它與登錄認證的關(guān)系較為獨立,因此,如果我們不使用這種基于用戶(hù)名與用戶(hù)組的授權檢查,也可以禁用這個(gè)模塊。
由于Cookie本身有過(guò)期的特點(diǎn),然而為了安全,FormsAuthenticationTicket也支持過(guò)期策略,不過(guò),ASP.NET的默認設置支持FormsAuthenticationTicket的可調過(guò)期行為,即:slidingExpiration=true 。這二者任何一個(gè)過(guò)期時(shí),都將導致登錄狀態(tài)無(wú)效。
FormsAuthenticationTicket的可調過(guò)期的主要判斷邏輯由FormsAuthentication.RenewTicketIfOld方法實(shí)現,代碼如下:
public static FormsAuthenticationTicket RenewTicketIfOld(FormsAuthenticationTicket tOld){ // 這段代碼是意思是:當指定的超時(shí)時(shí)間逝去大半時(shí)將更新FormsAuthenticationTicket對象。 if( tOld == null ) return null; DateTime now = DateTime.Now; TimeSpan span = (TimeSpan)(now - tOld.IssueDate); TimeSpan span2 = (TimeSpan)(tOld.Expiration - now); if( span2 > span ) return tOld; return new FormsAuthenticationTicket(tOld.Version, tOld.Name, now, now + (tOld.Expiration - tOld.IssueDate), tOld.IsPersistent, tOld.UserData, tOld.CookiePath);}
Request.IsAuthenticated可以告訴我們當前請求是否已經(jīng)過(guò)身份驗證,我們來(lái)看一下這個(gè)屬性是如何實(shí)現的:
public bool IsAuthenticated{ get { return (((this._context.User != null) && (this._context.User.Identity != null)) && this._context.User.Identity.IsAuthenticated); }}
從代碼可以看出,它的返回結果基本上來(lái)源于對Context.User的判斷。
另外,由于User和Identity都是二個(gè)接口類(lèi)型的屬性,因此,不同的實(shí)現方式對返回值也有影響。
由于可能會(huì )經(jīng)常使用HttpContext.User這個(gè)實(shí)例屬性,為了讓它能正常使用,DefaultAuthenticationModule會(huì )在A(yíng)SP.NET管線(xiàn)的PostAuthenticateRequest事件中檢查此屬性是否為null,如果它為null,DefaultAuthenticationModule會(huì )給它一個(gè)默認的GenericPrincipal對象,此對象指示一個(gè)未登錄的用戶(hù)。
我認為ASP.NET的身份認證的最核心部分其實(shí)就是HttpContext.User這個(gè)屬性所指向的對象。
為了更好了理解Forms身份認證,我認為自己重新實(shí)現User這個(gè)對象的接口會(huì )有較好的幫助。
前面演示了最簡(jiǎn)單的ASP.NET Forms身份認證的實(shí)現方法,即:直接調用SetAuthCookie方法。不過(guò)調用這個(gè)方法,只能傳遞一個(gè)登錄名。但是有時(shí)候為了方便后續的請求處理,還需要保存一些與登錄名相關(guān)的額外信息。雖然知道ASP.NET使用Cookie來(lái)保存登錄名狀態(tài)信息,我們也可以直接將前面所說(shuō)的額外信息直接保存在Cookie中,但是考慮安全性,我們還需要設計一些加密方法,而且還需要考慮這些額外信息保存在哪里才能方便使用,并還要考慮隨登錄與注銷(xiāo)同步修改。因此,實(shí)現這些操作還是有點(diǎn)繁瑣的。
為了保存與登錄名相關(guān)的額外的用戶(hù)信息,我認為實(shí)現自定義的身份認證標識(HttpContext.User實(shí)例)是個(gè)容易的解決方法。
理解這個(gè)方法也會(huì )讓我們對Forms身份認證有著(zhù)更清楚地認識。
這個(gè)方法的核心是(分為二個(gè)子過(guò)程):
1. 在登錄時(shí),創(chuàng )建自定義的FormsAuthenticationTicket對象,它包含了用戶(hù)信息。
2. 加密FormsAuthenticationTicket對象。
3. 創(chuàng )建登錄Cookie,它將包含FormsAuthenticationTicket對象加密后的結果。
4. 在管線(xiàn)的早期階段,讀取登錄Cookie,如果有,則解密。
5. 從解密后的FormsAuthenticationTicket對象中還原我們保存的用戶(hù)信息。
6. 設置HttpContext.User為我們自定義的對象。
現在,我們還是來(lái)看一下HttpContext.User這個(gè)屬性的定義:
// 為當前 HTTP 請求獲取或設置安全信息。//// 返回結果:// 當前 HTTP 請求的安全信息。public IPrincipal User { get; set; }
由于這個(gè)屬性只是個(gè)接口類(lèi)型,因此,我們也可以自己實(shí)現這個(gè)接口。
考慮到更好的通用性:不同的項目可能要求接受不同的用戶(hù)信息類(lèi)型。所以,我定義了一個(gè)泛型類(lèi)。
public class MyFormsPrincipal<TUserData> : IPrincipal where TUserData : class, new(){ private IIdentity _identity; private TUserData _userData; public MyFormsPrincipal(FormsAuthenticationTicket ticket, TUserData userData) { if( ticket == null ) throw new ArgumentNullException("ticket"); if( userData == null ) throw new ArgumentNullException("userData"); _identity = new FormsIdentity(ticket); _userData = userData; } public TUserData UserData { get { return _userData; } } public IIdentity Identity { get { return _identity; } } public bool IsInRole(string role) { // 把判斷用戶(hù)組的操作留給UserData去實(shí)現。 IPrincipal principal = _userData as IPrincipal; if( principal == null ) throw new NotImplementedException(); else return principal.IsInRole(role); }
與之配套使用的用戶(hù)信息的類(lèi)型定義如下(可以根據實(shí)際情況來(lái)定義):
public class UserInfo : IPrincipal{ public int UserId; public int GroupId; public string UserName; // 如果還有其它的用戶(hù)信息,可以繼續添加。 public override string ToString() { return string.Format("UserId: {0}, GroupId: {1}, UserName: {2}, IsAdmin: {3}", UserId, GroupId, UserName, IsInRole("Admin")); } #region IPrincipal Members [ScriptIgnore] public IIdentity Identity { get { throw new NotImplementedException(); } } public bool IsInRole(string role) { if( string.Compare(role, "Admin", true) == 0 ) return GroupId == 1; else return GroupId > 0; } #endregion}
注意:表示用戶(hù)信息的類(lèi)型并不要求一定要實(shí)現IPrincipal接口,如果不需要用戶(hù)組的判斷,可以不實(shí)現這個(gè)接口。
登錄時(shí)需要調用的方法(定義在MyFormsPrincipal類(lèi)型中):
/// <summary>/// 執行用戶(hù)登錄操作/// </summary>/// <param name="loginName">登錄名</param>/// <param name="userData">與登錄名相關(guān)的用戶(hù)信息</param>/// <param name="expiration">登錄Cookie的過(guò)期時(shí)間,單位:分鐘。</param>public static void SignIn(string loginName, TUserData userData, int expiration){ if( string.IsNullOrEmpty(loginName) ) throw new ArgumentNullException("loginName"); if( userData == null ) throw new ArgumentNullException("userData"); // 1. 把需要保存的用戶(hù)數據轉成一個(gè)字符串。 string data = null; if( userData != null ) data = (new JavaScriptSerializer()).Serialize(userData); // 2. 創(chuàng )建一個(gè)FormsAuthenticationTicket,它包含登錄名以及額外的用戶(hù)數據。 FormsAuthenticationTicket ticket = new FormsAuthenticationTicket( 2, loginName, DateTime.Now, DateTime.Now.AddDays(1), true, data); // 3. 加密Ticket,變成一個(gè)加密的字符串。 string cookieValue = FormsAuthentication.Encrypt(ticket); // 4. 根據加密結果創(chuàng )建登錄Cookie HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, cookieValue); cookie.HttpOnly = true; cookie.Secure = FormsAuthentication.RequireSSL; cookie.Domain = FormsAuthentication.CookieDomain; cookie.Path = FormsAuthentication.FormsCookiePath; if( expiration > 0 ) cookie.Expires = DateTime.Now.AddMinutes(expiration); HttpContext context = HttpContext.Current; if( context == null ) throw new InvalidOperationException(); // 5. 寫(xiě)登錄Cookie context.Response.Cookies.Remove(cookie.Name); context.Response.Cookies.Add(cookie);}
這里有必要再補充一下:登錄狀態(tài)是有過(guò)期限制的。Cookie有 有效期,FormsAuthenticationTicket對象也有 有效期。這二者任何一個(gè)過(guò)期時(shí),都將導致登錄狀態(tài)無(wú)效。按照默認設置,FormsAuthenticationModule將采用slidingExpiration=true的策略來(lái)處理FormsAuthenticationTicket過(guò)期問(wèn)題。
登錄頁(yè)面代碼:
<fieldset><legend>包含【用戶(hù)信息】的自定義登錄</legend> <form action="<%= Request.RawUrl %>" method="post"> <table border="0"> <tr><td>登錄名:</td> <td><input type="text" name="loginName" style="width: 200px" value="Fish" /></td></tr> <tr><td>UserId:</td> <td><input type="text" name="UserId" style="width: 200px" value="78" /></td></tr> <tr><td>GroupId:</td> <td><input type="text" name="GroupId" style="width: 200px" /> 1表示管理員用戶(hù) </td></tr> <tr><td>用戶(hù)全名:</td> <td><input type="text" name="UserName" style="width: 200px" value="Fish Li" /></td></tr> </table> <input type="submit" name="CustomizeLogin" value="登錄" /></form></fieldset>
登錄處理代碼:
public void CustomizeLogin(){ // ----------------------------------------------------------------- // 注意:演示代碼為了簡(jiǎn)單,這里不檢查用戶(hù)名與密碼是否正確。 // ----------------------------------------------------------------- string loginName = Request.Form["loginName"]; if( string.IsNullOrEmpty(loginName) ) return; UserInfo userinfo = new UserInfo(); int.TryParse(Request.Form["UserId"], out userinfo.UserId); int.TryParse(Request.Form["GroupId"], out userinfo.GroupId); userinfo.UserName = Request.Form["UserName"]; // 登錄狀態(tài)100分鐘內有效 MyFormsPrincipal<UserInfo>.SignIn(loginName, userinfo, 100); TryRedirect();}
顯示用戶(hù)信息的頁(yè)面代碼:
<fieldset><legend>用戶(hù)狀態(tài)</legend><form action="<%= Request.RawUrl %>" method="post"> <% if( Request.IsAuthenticated ) { %> 當前用戶(hù)已登錄,登錄名:<%= Context.User.Identity.Name.HtmlEncode() %> <br /> <% var user = Context.User as MyFormsPrincipal<UserInfo>; %> <% if( user != null ) { %> <%= user.UserData.ToString().HtmlEncode() %> <% } %> <input type="submit" name="Logon" value="退出" /> <% } else { %> <b>當前用戶(hù)還未登錄。</b> <% } %> </form></fieldset>
為了能讓上面的頁(yè)面代碼發(fā)揮工作,必須在頁(yè)面顯示前重新設置HttpContext.User對象。
為此,我在Global.asax中添加了一個(gè)事件處理器:
protected void Application_AuthenticateRequest(object sender, EventArgs e){ HttpApplication app = (HttpApplication)sender; MyFormsPrincipal<UserInfo>.TrySetUserInfo(app.Context);}
TrySetUserInfo的實(shí)現代碼:
/// <summary>/// 根據HttpContext對象設置用戶(hù)標識對象/// </summary>/// <param name="context"></param>public static void TrySetUserInfo(HttpContext context){ if( context == null ) throw new ArgumentNullException("context"); // 1. 讀登錄Cookie HttpCookie cookie = context.Request.Cookies[FormsAuthentication.FormsCookieName]; if( cookie == null || string.IsNullOrEmpty(cookie.Value) ) return; try { TUserData userData = null; // 2. 解密Cookie值,獲取FormsAuthenticationTicket對象 FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value); if( ticket != null && string.IsNullOrEmpty(ticket.UserData) == false ) // 3. 還原用戶(hù)數據 userData = (new JavaScriptSerializer()).Deserialize<TUserData>(ticket.UserData); if( ticket != null && userData != null ) // 4. 構造我們的MyFormsPrincipal實(shí)例,重新給context.User賦值。 context.User = new MyFormsPrincipal<TUserData>(ticket, userData); } catch { /* 有異常也不要拋出,防止攻擊者試探。 */ }}
默認情況下,ASP.NET 生成隨機密鑰并將其存儲在本地安全機構 (LSA) 中,因此,當需要在多臺機器之間使用Forms身份認證時(shí),就不能再使用隨機生成密鑰的方式,需要我們手工指定,保證每臺機器的密鑰是一致的。
用于Forms身份認證的密鑰可以在web.config的machineKey配置節中指定,我們還可以指定加密解密算法:
<machineKey decryption="Auto" [Auto | DES | 3DES | AES] decryptionKey="AutoGenerate,IsolateApps" [String]/>
關(guān)于這二個(gè)屬性,MSDN有如下解釋?zhuān)?/p>


這一小節送給所有對自動(dòng)化測試感興趣的朋友。
有時(shí)我們需要用代碼訪(fǎng)問(wèn)某些頁(yè)面,比如:希望用代碼測試服務(wù)端的響應。
如果是簡(jiǎn)單的頁(yè)面,或者頁(yè)面允許所有客戶(hù)端訪(fǎng)問(wèn),這樣不會(huì )有問(wèn)題,但是,如果此時(shí)我們要訪(fǎng)問(wèn)的頁(yè)面是一個(gè)受限頁(yè)面,那么就必須也要像人工操作那樣:先訪(fǎng)問(wèn)登錄頁(yè)面,提交登錄數據,獲取服務(wù)端生成的登錄Cookie,接下來(lái)才能去訪(fǎng)問(wèn)其它的受限頁(yè)面(但要帶上登錄Cookie)。
注意:由于登錄Cookie通常是加密的,且會(huì )發(fā)生變化,因此直接在代碼中硬編碼指定登錄Cookie會(huì )導致代碼難以維護。
在前面的示例中,我已在web.config為MyInfo.aspx設置過(guò)禁止匿名訪(fǎng)問(wèn),如果我用下面的代碼去調用:
private static readonly string MyInfoPageUrl = "http://localhost:51855/MyInfo.aspx";static void Main(string[] args){ // 這個(gè)調用得到的結果其實(shí)是default.aspx頁(yè)面的輸出,并非MyInfo.aspx HttpWebRequest request = MyHttpClient.CreateHttpWebRequest(MyInfoPageUrl); string html = MyHttpClient.GetResponseText(request); if( html.IndexOf("<span>Fish</span>") > 0 ) Console.WriteLine("調用成功。"); else Console.WriteLine("頁(yè)面結果不符合預期。");}
此時(shí),輸出的結果將會(huì )是:
頁(yè)面結果不符合預期。
如果我用下面的代碼:
private static readonly string LoginUrl = "http://localhost:51855/default.aspx";private static readonly string MyInfoPageUrl = "http://localhost:51855/MyInfo.aspx";static void Main(string[] args){ // 創(chuàng )建一個(gè)CookieContainer實(shí)例,供多次請求之間共享Cookie CookieContainer cookieContainer = new CookieContainer(); // 首先去登錄頁(yè)面登錄 MyHttpClient.HttpPost(LoginUrl, "NormalLogin=aa&loginName=Fish", cookieContainer); // 此時(shí)cookieContainer已經(jīng)包含了服務(wù)端生成的登錄Cookie // 再去訪(fǎng)問(wèn)要請求的頁(yè)面。 string html = MyHttpClient.HttpGet(MyInfoPageUrl, cookieContainer); if( html.IndexOf("<span>Fish</span>") > 0 ) Console.WriteLine("調用成功。"); else Console.WriteLine("頁(yè)面結果不符合預期。"); // 如果還要訪(fǎng)問(wèn)其它的受限頁(yè)面,可以繼續調用。}
此時(shí),輸出的結果將會(huì )是:
調用成功。
說(shuō)明:在改進(jìn)的版本中,我首先創(chuàng )建一個(gè)CookieContainer實(shí)例,它可以在HTTP調用過(guò)程中接收服務(wù)器產(chǎn)生的Cookie,并能在發(fā)送HTTP請求時(shí)將已經(jīng)保存的Cookie再發(fā)送給服務(wù)端。在創(chuàng )建好CookieContainer實(shí)例之后,每次使用HttpWebRequest對象時(shí),只要將CookieContainer實(shí)例賦值給HttpWebRequest對象的CookieContainer屬性,即可實(shí)現在多次的HTTP調用中Cookie的接收與發(fā)送,最終可以模擬瀏覽器的Cookie處理行為,服務(wù)端也能正確識別客戶(hù)的身份。
ASP.NET Forms身份認證就說(shuō)到這里,如果您對ASP.NET Windows身份認證有興趣,那么請關(guān)注我的后續博客。
如果,您認為閱讀這篇博客讓您有些收獲,不妨點(diǎn)擊一下右下角的【推薦】按鈕。
如果,您希望更容易地發(fā)現我的新博客,不妨點(diǎn)擊一下右下角的【關(guān)注 Fish Li】。
因為,我的寫(xiě)作熱情也離不開(kāi)您的肯定支持。
感謝您的閱讀,如果您對我的博客所講述的內容有興趣,請繼續關(guān)注我的后續博客,我是Fish Li 。
聯(lián)系客服