天天看點

細說ASP.NET Forms身份認證

使用者登入是個很常見的業務需求,在ASP.NET中,這個過程被稱為身份認證。 由于很常見,是以,我認為把這塊内容整理出來,與大家分享應該是件有意義的事。

在開發ASP.NET項目中,我們最常用的是Forms認證,也叫【表單認證】。 這種認證方式既可以用于區域網路環境,也可用于網際網路環境,是以,它有着非常廣泛的使用。 這篇部落客要讨論的話題是:ASP.NET Forms 身份認證。

有一點我要申明一下:在這篇部落格中,不會涉及ASP.NET的登入系列控件以及membership的相關話題, 我隻想用比較原始的方式來說明在ASP.NET中是如何實作身份認證的過程。

回到頂部

ASP.NET身份認證基礎

在開始今天的部落格之前,我想有二個最基礎的問題首先要明确:

1. 如何判斷目前請求是一個已登入使用者發起的?

2. 如何擷取目前登入使用者的登入名?

在标準的ASP.NET身份認證方式中,上面二個問題的答案是:

1. 如果Request.IsAuthenticated為true,則表示是一個已登入使用者。

2. 如果是一個已登入使用者,通路HttpContext.User.Identity.Name可擷取登入名(都是執行個體屬性)。

接下來,本文将會圍繞上面二個問題展開,請繼續閱讀。

ASP.NET身份認證過程

在ASP.NET中,整個身份認證的過程其實可分為二個階段:認證與授權。

1. 認證階段:識别目前請求的使用者是不是一個可識别(的已登入)使用者。

2. 授權階段:是否允許目前請求通路指定的資源。

這二個階段在ASP.NET管線中用AuthenticateRequest和AuthorizeRequest事件來表示。

在認證階段,ASP.NET會檢查目前請求,根據web.config設定的認證方式,嘗試構造HttpContext.User對象供我們在後續的進行中使用。 在授權階段,會檢查目前請求所通路的資源是否允許通路,因為有些受保護的頁面資源可能要求特定的使用者或者使用者組才能通路。 是以,即使是一個已登入使用者,也有可能會不能通路某些頁面。 當發現使用者不能通路某個頁面資源時,ASP.NET會将請求重定向到登入頁面。

受保護的頁面與登入頁面我們都可以在web.config中指定,具體方法可參考後文。

在ASP.NET中,Forms認證是由FormsAuthenticationModule實作的,URL的授權檢查是由UrlAuthorizationModule實作的。

如何實作登入與登出

前面我介紹了可以使用Request.IsAuthenticated來判斷目前使用者是不是一個已登入使用者,那麼這一過程又是如何實作的呢?

為了回答這個問題,我準備了一個簡單的示例頁面,代碼如下:

<fieldset><legend>使用者狀态</legend><form action="<%= Request.RawUrl %>" method="post">
    <% if( Request.IsAuthenticated ) { %>
        目前使用者已登入,登入名:<%= Context.User.Identity.Name.HtmlEncode() %> <br />            
        <input type="submit" name="Logon" value="退出" />
    <% } else { %>
        <b>目前使用者還未登入。</b>
    <% } %>            
</form></fieldset>
      

頁面顯示效果如下:

細說ASP.NET Forms身份認證

根據前面的代碼,我想現在能看到這個頁面顯示也是正确的,是的,我目前還沒有登入(根本還沒有實作這個功能)。

下面我再加點代碼來實作使用者登入。頁面代碼:

<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>
      

現在頁面的顯示效果:

細說ASP.NET Forms身份認證

登入與登出的實作代碼:

public void Logon()
{
    FormsAuthentication.SignOut();
}

public void NormalLogin()
{
    // -----------------------------------------------------------------
    // 注意:示範代碼為了簡單,這裡不檢查使用者名與密碼是否正确。
    // -----------------------------------------------------------------

    string loginName = Request.Form["loginName"];
    if( string.IsNullOrEmpty(loginName) )
        return;
    
    FormsAuthentication.SetAuthCookie(loginName, true);

    TryRedirect();
}
      

現在,我可試一下登入功能。點選登入按鈕後,頁面的顯示效果如下:

細說ASP.NET Forms身份認證

從圖檔的顯示可以看出,我前面寫的NormalLogin()方法确實可以實作使用者登入。

當然了,我也可以在此時點選退出按鈕,那麼就回到了圖檔2的顯示。

寫到這裡,我想有必要再來總結一下在ASP.NET中實作登入與登出的方法:

1. 登入:調用FormsAuthentication.SetAuthCookie()方法,傳遞一個登入名即可。

2. 登出:調用FormsAuthentication.SignOut()方法。

保護受限制的頁面

在一個ASP.NET網站中,有些頁面會允許所有使用者通路,包括一些未登入使用者,但有些頁面則必須是已登入使用者才能通路, 還有一些頁面可能會要求特定的使用者或者使用者組的成員才能通路。 這類頁面是以也可稱為【受限頁面】,它們一般代表着比較重要的頁面,包含一些重要的操作或功能。

為了保護受限制的頁面的通路,ASP.NET提供了一種簡單的方式: 可以在web.config中指定受限資源允許哪些使用者或者使用者組(角色)的通路,也可以設定為禁止通路。

比如,網站有一個頁面:MyInfo.aspx,它要求通路這個頁面的通路者必須是一個已登入使用者,那麼可以在web.config中這樣配置:

<location path="MyInfo.aspx">
    <system.web>
        <authorization>
            <deny users="?"/>
        </authorization>
    </system.web>
</location>
      

為了友善,我可能會将一些管理相關的多個頁面放在Admin目錄中,顯然這些頁面隻允許Admin使用者組的成員才可以通路。 對于這種情況,我們可以直接針對一個目錄設定通路規則:

<location path="Admin">
    <system.web>
        <authorization>
            <allow roles="Admin"/>
            <deny users="*"/>
        </authorization>
    </system.web>
</location>
      

這樣就不必一個一個頁面單獨設定了,還可以在目錄中建立一個web.config來指定目錄的通路規則,請參考後面的示例。

在前面的示例中,有一點要特别注意的是:

1. allow和deny之間的順序一定不能寫錯了,UrlAuthorizationModule将按這個順序依次判斷。

2. 如果某個資源隻允許某類使用者通路,那麼最後的一條規則一定是 <deny users="*" />

在allow和deny的配置中,我們可以在一條規則中指定多個使用者:

1. 使用users屬性,值為逗号分隔的使用者名清單。

2. 使用roles屬性,值為逗号分隔的角色清單。

3. 問号 (?) 表示匿名使用者。

4. 星号 (*) 表示所有使用者。

登入頁不能正常顯示的問題

有時候,我們可能要開發一個内部使用的網站程式,這類網站程式要求 禁止匿名使用者的通路,即:所有使用者必須先登入才能通路。 是以,我們通常會在網站根目錄下的web.config中這樣設定:

<authorization>
    <deny users="?"/>
</authorization>
      

對于我們的示例,我們也可以這樣設定。此時在浏覽器打開頁面時,呈現效果如下:

細說ASP.NET Forms身份認證

從圖檔中可以看出:頁面的樣式顯示不正确,最下邊還多出了一行文字。

這個頁面的完整代碼是這樣的(它引用了一個CSS檔案和一個JS檔案): 

細說ASP.NET Forms身份認證

頁面最後一行文字平時不顯示是因為JScript.js中有以下代碼:

document.getElementById("hideText").setAttribute("style", "display: none");

      

這段JS代碼能做什麼,我想就不用再解釋了。

雖然這段JS代碼沒什麼價值,但我主要是想示範在登入頁面中引用JS的場景。

根據前面圖檔,我們可以猜測到:應該是CSS和JS檔案沒有正确加載造成的。

為了确認就是這樣原因,我們可以打開FireBug再來看一下頁面加載情況:

細說ASP.NET Forms身份認證

根據FireBug提供的線索我們可以分析出,頁面在通路CSS, JS檔案時,其實是被重定向到登入頁面了,是以獲得的結果肯定也是無意義的, 是以就造成了登入頁的顯示不正确。

還記得前所說的【授權】嗎?

是的,現在就是由于我們在web.config中設定了不允許匿名使用者通路,是以,所有的資源也就不允許匿名使用者通路了, 包括登入頁所引用的CSS, JS檔案。當授權檢查失敗時,請求會被重定向到登入頁面, 是以,登入頁本身所引用的CSS, JS檔案最後得到的響應内容其實是登入頁的HTML代碼, 最終導緻它們不能發揮作用,表現為登入頁的樣式顯示不正确,以及引用的JS檔案也不起作用。

不過,有一點比較奇怪:為什麼通路登入頁面時,沒有發生重定向呢?

原因是這樣的:在ASP.NET内部,當發現是在通路登入面時,會設定HttpContext.SkipAuthorization = true (其實是一個内部調用), 這樣的設定會告訴後面的授權檢查子產品:跳過這次請求的授權檢查。 是以,登入頁總是允許所有使用者通路,但是CSS檔案以及JS檔案是在另外的請求中發生的,那些請求并不會要跳過授權子產品的檢查。

為了解決登入頁不能正确顯示的問題,我們可以這樣處理:

1. 在網站根目錄中的web.config中設定登入頁所引用的JS, CSS檔案都允許匿名通路。

2. 也可以直接針對JS, CSS目錄設定為允許匿名使用者通路。

3. 還可以在CSS, JS目錄中建立一個web.config檔案來配置對應目錄的授權規則。可參考以下web.config檔案:

<?xml version="1.0"?>
<configuration>
    <system.web>
        <authorization>
            <allow users="*"/>
        </authorization>
    </system.web>
</configuration>

      

第三種做法可以不修改網站根目錄下的web.config檔案。

注意:在IIS中看到的情況就和在Visual Studio中看到的結果就不一樣了。 因為,像js, css, image這類檔案屬于靜态資源檔案,IIS能直接處理,不需要交給ASP.NET來響應,是以就不會發生授權檢查失敗, 是以,如果這類網站部署在IIS中,看到的結果又是正常的。

認識Forms身份認證

前面我示範了如何用代碼實作登入與登出的過程,下面再來看一下登入時,ASP.NET到底做了些什麼事情, 它是如何知道目前請求是一個已登入使用者的?

在繼續探索這個問題前,我想有必要來了解一下HTTP協定的一些特點。

HTTP是一個無狀态的協定,無狀态的意思可以了解為: WEB伺服器在處理所有傳入請求時,根本就不知道某個請求是否是一個使用者的第一次請求與後續請求,或者是另一個使用者的請求。 WEB伺服器每次在處理請求時,都會按照使用者所通路的資源所對應的處理代碼,從頭到尾執行一遍,然後輸出響應内容, WEB伺服器根本不會記住已處理了哪些使用者的請求,是以,我們通常說HTTP協定是無狀态的。

雖然HTTP協定與WEB伺服器是無狀态,但我們的業務需求卻要求有狀态,典型的就是使用者登入, 在這種業務需求中,要求WEB伺服器端能區分某個請求是不是一個已登入使用者發起的,或者目前請求是哪個使用者發出的。 在開發WEB應用程式時,我們通常會使用Cookie來儲存一些簡單的資料供服務端維持必要的狀态。 既然這是個通常的做法,那我們現在就來看一下現在頁面的Cookie使用情況吧,以下是我用FireFox所看到的Cookie清單:

細說ASP.NET Forms身份認證

這個名字:LoginCookieName,是我在web.config中指定的:

<authentication mode="Forms" >
    <forms cookieless="UseCookies" name="LoginCookieName" loginUrl="~/Default.aspx"></forms>
</authentication>
      

在這段配置中,我不僅指定的登入狀态的Cookie名,還指定了身份驗證模式,以及Cookie的使用方式。

為了判斷這個Cookie是否與登入狀态有關,我們可以在浏覽器提供的界面删除它,然後重新整理頁面,此時頁面的顯示效果如下:

細說ASP.NET Forms身份認證

此時,頁面顯示目前使用者沒有登入。

為了确認這個Cookie與登入狀态有關,我們可以重新登入,然後再登出。

發現隻要是頁面顯示目前使用者未登入時,這個Cookie就不會存在。

事實上,通過SetAuthCookie這個方法名,我們也可以猜得出這個操作會寫一個Cookie。

注意:本文不讨論無Cookie模式的Forms登入。

從前面的截圖我們可以看出:雖然目前使用者名是 Fish ,但是,Cookie的值是一串亂碼樣的字元串。

由于安全性的考慮,ASP.NET對Cookie做過加密處理了,這樣可以防止惡意使用者構造Cookie繞過登入機制來模拟登入使用者。 如果想知道這串加密字元串是如何得到的,那麼請參考後文。

小結:

1. Forms身份認證是在web.config中指定的,我們還可以設定Forms身份認證的其它配置參數。

2. Forms身份認證的登入狀态是通過Cookie來維持的。

3. Forms身份認證的登入Cookie是加密的。

了解Forms身份認證

經過前面的Cookie分析,我們可以發現Cookie的值是一串加密後的字元串, 現在我們就來分析這個加密過程以及Cookie對于身份認證的作用。

登入的操作通常會檢查使用者提供的使用者名和密碼,是以登入狀态也必須具有足夠高的安全性。 在Forms身份認證中,由于登入狀态是儲存在Cookie中,而Cookie又會儲存到用戶端,是以,為了保證登入狀态不被惡意使用者僞造, ASP.NET采用了加密的方式儲存登入狀态。 為了實作安全性,ASP.NET采用【Forms身份驗證憑據】(即FormsAuthenticationTicket對象)來表示一個Forms登入使用者, 加密與解密由FormsAuthentication的Encrypt與Decrypt的方法來實作。

使用者登入的過程大緻是這樣的:

1. 檢查使用者送出的登入名和密碼是否正确。

2. 根據登入名建立一個FormsAuthenticationTicket對象。

3. 調用FormsAuthentication.Encrypt()加密。

4. 根據加密結果建立登入Cookie,并寫入Response。

在登入驗證結束後,一般會産生重定向操作, 那麼後面的每次請求将帶上前面産生的加密Cookie,供伺服器來驗證每次請求的登入狀态。

每次請求時的(認證)處理過程如下:

1. FormsAuthenticationModule嘗試讀取登入Cookie。

2. 從Cookie中解析出FormsAuthenticationTicket對象。過期的對象将被忽略。

3. 根據FormsAuthenticationTicket對象構造FormsIdentity對象并設定HttpContext.User

4. UrlAuthorizationModule執行授權檢查。

在登入與認證的實作中,FormsAuthenticationTicket和FormsAuthentication是二個核心的類型, 前者可以認為是一個資料結構,後者可認為是處理前者的工具類。

UrlAuthorizationModule是一個授權檢查子產品,其實它與登入認證的關系較為獨立, 是以,如果我們不使用這種基于使用者名與使用者組的授權檢查,也可以禁用這個子產品。

由于Cookie本身有過期的特點,然而為了安全,FormsAuthenticationTicket也支援過期政策, 不過,ASP.NET的預設設定支援FormsAuthenticationTicket的可調過期行為,即:slidingExpiration=true 。 這二者任何一個過期時,都将導緻登入狀态無效。

FormsAuthenticationTicket的可調過期的主要判斷邏輯由FormsAuthentication.RenewTicketIfOld方法實作,代碼如下: 

細說ASP.NET Forms身份認證

Request.IsAuthenticated可以告訴我們目前請求是否已經過身份驗證, 我們來看一下這個屬性是如何實作的:

public bool IsAuthenticated
{
    get
    {
        return (((this._context.User != null) 
            && (this._context.User.Identity != null)) 
            && this._context.User.Identity.IsAuthenticated);
    }
}
      

從代碼可以看出,它的傳回結果基本上來源于對Context.User的判斷。

另外,由于User和Identity都是二個接口類型的屬性,是以,不同的實作方式對傳回值也有影響。

由于可能會經常使用HttpContext.User這個執行個體屬性,為了讓它能正常使用, DefaultAuthenticationModule會在ASP.NET管線的PostAuthenticateRequest事件中檢查此屬性是否為null, 如果它為null,DefaultAuthenticationModule會給它一個預設的GenericPrincipal對象,此對象訓示一個未登入的使用者。

我認為ASP.NET的身份認證的最核心部分其實就是HttpContext.User這個屬性所指向的對象。

為了更好了了解Forms身份認證,我認為自己重新實作User這個對象的接口會有較好的幫助。

實作自定義的身份認證辨別

前面示範了最簡單的ASP.NET Forms身份認證的實作方法,即:直接調用SetAuthCookie方法。 不過調用這個方法,隻能傳遞一個登入名。 但是有時候為了友善後續的請求處理,還需要儲存一些與登入名相關的額外資訊。 雖然知道ASP.NET使用Cookie來儲存登入名狀态資訊,我們也可以直接将前面所說的額外資訊直接儲存在Cookie中, 但是考慮安全性,我們還需要設計一些加密方法,而且還需要考慮這些額外資訊儲存在哪裡才能友善使用, 并還要考慮随登入與登出同步修改。 是以,實作這些操作還是有點繁瑣的。

為了儲存與登入名相關的額外的使用者資訊,我認為實作自定義的身份認證辨別(HttpContext.User執行個體)是個容易的解決方法。

了解這個方法也會讓我們對Forms身份認證有着更清楚地認識。

這個方法的核心是(分為二個子過程):

1. 在登入時,建立自定義的FormsAuthenticationTicket對象,它包含了使用者資訊。

2. 加密FormsAuthenticationTicket對象。

3. 建立登入Cookie,它将包含FormsAuthenticationTicket對象加密後的結果。

4. 在管線的早期階段,讀取登入Cookie,如果有,則解密。

5. 從解密後的FormsAuthenticationTicket對象中還原我們儲存的使用者資訊。

6. 設定HttpContext.User為我們自定義的對象。

現在,我們還是來看一下HttpContext.User這個屬性的定義:

//  為目前 HTTP 請求擷取或設定安全資訊。
//
// 傳回結果:
//     目前 HTTP 請求的安全資訊。
public IPrincipal User { get; set; }
      

由于這個屬性隻是個接口類型,是以,我們也可以自己實作這個接口。

考慮到更好的通用性:不同的項目可能要求接受不同的使用者資訊類型。是以,我定義了一個泛型類。

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)
    {
        // 把判斷使用者組的操作留給UserData去實作。

        IPrincipal principal = _userData as IPrincipal;
        if( principal == null )
            throw new NotImplementedException();
        else
            return principal.IsInRole(role);
    }
      

與之配套使用的使用者資訊的類型定義如下(可以根據實際情況來定義): 

細說ASP.NET Forms身份認證

注意:表示使用者資訊的類型并不要求一定要實作IPrincipal接口,如果不需要使用者組的判斷,可以不實作這個接口。

登入時需要調用的方法(定義在MyFormsPrincipal類型中):

/// <summary>
/// 執行使用者登入操作
/// </summary>
/// <param name="loginName">登入名</param>
/// <param name="userData">與登入名相關的使用者資訊</param>
/// <param name="expiration">登入Cookie的過期時間,機關:分鐘。</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. 把需要儲存的使用者資料轉成一個字元串。
    string data = null;
    if( userData != null )
        data = (new JavaScriptSerializer()).Serialize(userData);


    // 2. 建立一個FormsAuthenticationTicket,它包含登入名以及額外的使用者資料。
    FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
        2, loginName, DateTime.Now, DateTime.Now.AddDays(1), true, data);


    // 3. 加密Ticket,變成一個加密的字元串。
    string cookieValue = FormsAuthentication.Encrypt(ticket);


    // 4. 根據加密結果建立登入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);
}
      

這裡有必要再補充一下: 登入狀态是有過期限制的。Cookie有 有效期,FormsAuthenticationTicket對象也有 有效期。 這二者任何一個過期時,都将導緻登入狀态無效。 按照預設設定,FormsAuthenticationModule将采用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()
{
    // -----------------------------------------------------------------
    // 注意:示範代碼為了簡單,這裡不檢查使用者名與密碼是否正确。
    // -----------------------------------------------------------------

    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"];

    // 登入狀态100分鐘内有效
    MyFormsPrincipal<UserInfo>.SignIn(loginName, userinfo, 100);

    TryRedirect();
}
      

顯示使用者資訊的頁面代碼:

<fieldset><legend>使用者狀态</legend><form action="<%= Request.RawUrl %>" method="post">
    <% if( Request.IsAuthenticated ) { %>
        目前使用者已登入,登入名:<%= 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>目前使用者還未登入。</b>
    <% } %>            
</form></fieldset>
      

為了能讓上面的頁面代碼發揮工作,必須在頁面顯示前重新設定HttpContext.User對象。

為此,我在Global.asax中添加了一個事件處理器:

protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
    HttpApplication app = (HttpApplication)sender;
    MyFormsPrincipal<UserInfo>.TrySetUserInfo(app.Context);
}
      

TrySetUserInfo的實作代碼:

/// <summary>
/// 根據HttpContext對象設定使用者辨別對象
/// </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. 還原使用者資料
            userData = (new JavaScriptSerializer()).Deserialize<TUserData>(ticket.UserData);

        if( ticket != null && userData != null )
            // 4. 構造我們的MyFormsPrincipal執行個體,重新給context.User指派。
            context.User = new MyFormsPrincipal<TUserData>(ticket, userData);
    }
    catch { /* 有異常也不要抛出,防止攻擊者試探。 */ }
}
      

在多台伺服器之間使用Forms身份認證

預設情況下,ASP.NET 生成随機密鑰并将其存儲在本地安全機構 (LSA) 中, 是以,當需要在多台機器之間使用Forms身份認證時,就不能再使用随機生成密鑰的方式, 需要我們手工指定,保證每台機器的密鑰是一緻的。

用于Forms身份認證的密鑰可以在web.config的machineKey配置節中指定,我們還可以指定加密解密算法:

<machineKey 
  decryption="Auto" [Auto | DES | 3DES | AES]
  decryptionKey="AutoGenerate,IsolateApps" [String]
/>
      

關于這二個屬性,MSDN有如下解釋:

細說ASP.NET Forms身份認證
細說ASP.NET Forms身份認證

在用戶端程式中通路受限頁面

這一小節送給所有對自動化測試感興趣的朋友。

有時我們需要用代碼通路某些頁面,比如:希望用代碼測試服務端的響應。

如果是簡單的頁面,或者頁面允許所有用戶端通路,這樣不會有問題, 但是,如果此時我們要通路的頁面是一個受限頁面,那麼就必須也要像人工操作那樣: 先通路登入頁面,送出登入資料,擷取服務端生成的登入Cookie, 接下來才能去通路其它的受限頁面(但要帶上登入Cookie)。

注意:由于登入Cookie通常是加密的,且會發生變化,是以直接在代碼中寫死指定登入Cookie會導緻代碼難以維護。

在前面的示例中,我已在web.config為MyInfo.aspx設定過禁止匿名通路,如果我用下面的代碼去調用:

private static readonly string MyInfoPageUrl = "http://localhost:51855/MyInfo.aspx";

static void Main(string[] args)
{
    // 這個調用得到的結果其實是default.aspx頁面的輸出,并非MyInfo.aspx
    HttpWebRequest request = MyHttpClient.CreateHttpWebRequest(MyInfoPageUrl);
    string html = MyHttpClient.GetResponseText(request);

    if( html.IndexOf("<span>Fish</span>") > 0 )
        Console.WriteLine("調用成功。");
    else
        Console.WriteLine("頁面結果不符合預期。");
}
      

此時,輸出的結果将會是:

頁面結果不符合預期。

如果我用下面的代碼:

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)
{
    // 建立一個CookieContainer執行個體,供多次請求之間共享Cookie
    CookieContainer cookieContainer = new CookieContainer();

    // 首先去登入頁面登入
    MyHttpClient.HttpPost(LoginUrl, "NormalLogin=aa&loginName=Fish", cookieContainer);

    // 此時cookieContainer已經包含了服務端生成的登入Cookie

    // 再去通路要請求的頁面。
    string html = MyHttpClient.HttpGet(MyInfoPageUrl, cookieContainer);

    if( html.IndexOf("<span>Fish</span>") > 0 )
        Console.WriteLine("調用成功。");
    else
        Console.WriteLine("頁面結果不符合預期。");

    // 如果還要通路其它的受限頁面,可以繼續調用。
}
      

調用成功。

說明:在改進的版本中,我首先建立一個CookieContainer執行個體, 它可以在HTTP調用過程中接收伺服器産生的Cookie,并能在發送HTTP請求時将已經儲存的Cookie再發送給服務端。 在建立好CookieContainer執行個體之後,每次使用HttpWebRequest對象時, 隻要将CookieContainer執行個體指派給HttpWebRequest對象的CookieContainer屬性,即可實作在多次的HTTP調用中Cookie的接收與發送, 最終可以模拟浏覽器的Cookie處理行為,服務端也能正确識别客戶的身份。

 ASP.NET Forms身份認證就說到這裡,如果您對ASP.NET Windows身份認證有興趣,那麼請關注我的後續部落格。

轉自:http://www.cnblogs.com/fish-li/archive/2012/04/15/2450571.html