ASP.NET身份認(rèn)證基礎(chǔ)
在開始今天的內(nèi)容之前,我想有二個(gè)最基礎(chǔ)的問題首先要明確:
1. 如何判斷當(dāng)前請(qǐng)求是一個(gè)已登錄用戶發(fā)起的?
2. 如何獲取當(dāng)前登錄用戶的登錄名?
在標(biāo)準(zhǔn)的ASP.NET身份認(rèn)證方式中,上面二個(gè)問題的答案是:
1. 如果Request.IsAuthenticated為true,則表示是一個(gè)已登錄用戶。
2. 如果是一個(gè)已登錄用戶,訪問HttpContext.User.Identity.Name可獲取登錄名(都是實(shí)例屬性)。
接下來,本文將會(huì)圍繞上面二個(gè)問題展開,請(qǐng)繼續(xù)閱讀。
ASP.NET身份認(rèn)證過程
在ASP.NET中,整個(gè)身份認(rèn)證的過程其實(shí)可分為二個(gè)階段:認(rèn)證與授權(quán)。
1. 認(rèn)證階段:識(shí)別當(dāng)前請(qǐng)求的用戶是不是一個(gè)可識(shí)別(的已登錄)用戶。
2. 授權(quán)階段:是否允許當(dāng)前請(qǐng)求訪問指定的資源。
這二個(gè)階段在ASP.NET管線中用AuthenticateRequest和AuthorizeRequest事件來表示。
在認(rèn)證階段,ASP.NET會(huì)檢查當(dāng)前請(qǐng)求,根據(jù)web.config設(shè)置的認(rèn)證方式,嘗試構(gòu)造HttpContext.User對(duì)象供我們?cè)诤罄m(xù)的處理中使用。在授權(quán)階段,會(huì)檢查當(dāng)前請(qǐng)求所訪問的資源是否允許訪問,因?yàn)橛行┦鼙Wo(hù)的頁面資源可能要求特定的用戶或者用戶組才能訪問。所以,即使是一個(gè)已登錄用戶,也有可能會(huì)不能訪問某些頁面。當(dāng)發(fā)現(xiàn)用戶不能訪問某個(gè)頁面資源時(shí),ASP.NET會(huì)將請(qǐng)求重定向到登錄頁面。
受保護(hù)的頁面與登錄頁面我們都可以在web.config中指定,具體方法可參考后文。
在ASP.NET中,F(xiàn)orms認(rèn)證是由FormsAuthenticationModule實(shí)現(xiàn)的,URL的授權(quán)檢查是由UrlAuthorizationModule實(shí)現(xiàn)的。
如何實(shí)現(xiàn)登錄與注銷
前面我介紹了可以使用Request.IsAuthenticated來判斷當(dāng)前用戶是不是一個(gè)已登錄用戶,那么這一過程又是如何實(shí)現(xiàn)的呢?
為了回答這個(gè)問題,我準(zhǔn)備了一個(gè)簡(jiǎn)單的示例頁面,代碼如下:
fieldset>legend>用戶狀態(tài)/legend>form action="%= Request.RawUrl %>" method="post">
% if( Request.IsAuthenticated ) { %>
當(dāng)前用戶已登錄,登錄名:%= Context.User.Identity.Name.HtmlEncode() %> br />
input type="submit" name="Logon" value="退出" />
% } else { %>
b>當(dāng)前用戶還未登錄。/b>
% } %>
/form>/fieldset>
頁面顯示效果如下:

根據(jù)前面的代碼,我想現(xiàn)在能看到這個(gè)頁面顯示也是正確的,是的,我目前還沒有登錄(根本還沒有實(shí)現(xiàn)這個(gè)功能)。
下面我再加點(diǎn)代碼來實(shí)現(xiàn)用戶登錄。頁面代碼:
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>
現(xiàn)在頁面的顯示效果:

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

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

從圖片中可以看出:頁面的樣式顯示不正確,最下邊還多出了一行文字。
這個(gè)頁面的完整代碼是這樣的(它引用了一個(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>用戶狀態(tài)/legend>form action="%= Request.RawUrl %>" method="post">
% if( Request.IsAuthenticated ) { %>
當(dāng)前用戶已登錄,登錄名:%= Context.User.Identity.Name.HtmlEncode() %> br />
% var user = Context.User as MyFormsPrincipalUserInfo>; %>
% if( user != null ) { %>
%= user.UserData.ToString().HtmlEncode() %>
% } %>
input type="submit" name="Logon" value="退出" />
% } else { %>
b>當(dāng)前用戶還未登錄。/b>
% } %>
/form>/fieldset>
p id="hideText">i>不應(yīng)該顯示的文字/i>/p>
script type="text/javascript" src="js/JScript.js">/script>
/body>
/html>
頁面最后一行文字平時(shí)不顯示是因?yàn)镴Script.js中有以下代碼:
document.getElementById("hideText").setAttribute("style", "display: none");
這段JS代碼能做什么,我想就不用再解釋了。雖然這段JS代碼沒什么價(jià)值,但我主要是想演示在登錄頁面中引用JS的場(chǎng)景。
根據(jù)前面圖片,我們可以猜測(cè)到:應(yīng)該是CSS和JS文件沒有正確加載造成的。為了確認(rèn)就是這樣原因,我們可以打開FireBug再來看一下頁面加載情況:

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

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

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

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