注:不會涉及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實現的。
1. 登錄:調用FormsAuthentication.SetAuthCookie()方法,傳遞一個登錄名即可。
2. 注銷:調用FormsAuthentication.SignOut()方法。
保護受限制的頁面
為了保護受限制的頁面的訪問,ASP.NET提供了一種簡單的方式: 可以在web.config中指定受限資源允許哪些用戶或者用戶組(角色)的訪問,也可以設置為禁止訪問。
比如,網站有一個頁面:MyInfo.aspx,它要求訪問這個頁面的訪問者必須是一個已登錄用戶,那么可以在web.config中這樣配置:
<system.web>
<authorization>
<deny users="?"/>
</authorization>
</system.web>
</location>
為了方便,我可能會將一些管理相關的多個頁面放在Admin目錄中,顯然這些頁面只允許Admin用戶組的成員才可以訪問。 對於這種情況,我們可以直接針對一個目錄設置訪問規則:
<system.web>
<authorization>
<allow roles="Admin"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
這樣就不必一個一個頁面單獨設置了,還可以在目錄中創建一個web.config來指定目錄的訪問規則。在前面的示例中,有一點要特別注意的是:
1. allow和deny之間的順序一定不能寫錯了,UrlAuthorizationModule將按這個順序依次判斷。
2. 如果某個資源只允許某類用戶訪問,那么最后的一條規則一定是在allow和deny的配置中,我們可以在一條規則中指定多個用戶:
1. 使用users屬性,值為逗號分隔的用戶名列表。
2. 使用roles屬性,值為逗號分隔的角色列表。
3. 問號 (?) 表示匿名用戶。
4. 星號 (*) 表示所有用戶。
在ASP.NET內部,當發現是在訪問登錄面時,會設置HttpContext.SkipAuthorization = true (其實是一個內部調用), 這樣的設置會告訴后面的授權檢查模塊:跳過這次請求的授權檢查。因此,登錄頁總是允許所有用戶訪問,但是登錄頁所引用的CSS文件以及JS文件(頁面在訪問CSS, JS文件時,其實是被重定向到登錄頁面了) 是在另外的請求中發生的,那些請求並不會要跳過授權模塊的檢查。
為了解決登錄頁不能正確顯示的問題,我們可以這樣處理:
1. 在網站根目錄中的web.config中設置登錄頁所引用的JS, CSS文件都允許匿名訪問。
2. 也可以直接針對JS, CSS目錄設置為允許匿名用戶訪問。
3. 還可以在CSS, JS目錄中創建一個web.config文件來配置對應目錄的授權規則。可參考以下web.config文件:
<configuration>
<system.web>
<authorization>
<allow users="*"/>
</authorization>
</system.web>
</configuration>
注意:在IIS中看到的情況就和在Visual Studio中看到的結果就不一樣了。因為,像js, css, image這類文件屬於靜態資源文件,IIS能直接處理,不需要交給ASP.NET來響應,因此就不會發生授權檢查失敗, 所以,如果這類網站部署在IIS中,看到的結果又是正常的。
認識Forms身份認證
HTTP是一個無狀態的協議, 在開發WEB應用程序時,我們通常會使用Cookie來保存一些簡單的數據供服務端維持必要的狀態。以下是我用FireFox所看到的Cookie列表:
這個名字:LoginCookieName,是在web.config中指定的:
<forms cookieless="UseCookies" name="LoginCookieName" loginUrl="~/Default.aspx"></forms>
</authentication>
理解Forms身份認證
為了實現安全性,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方法實現,代碼如下:

public static FormsAuthenticationTicket RenewTicketIfOld(FormsAuthenticationTicket tOld) { // 這段代碼是意思是:當指定的超時時間逝去大半時將更新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); }
在多台服務器之間使用Forms身份認證(Passport單點身份驗證)
默認情況下,ASP.NET 生成隨機密鑰並將其存儲在本地安全機構 (LSA) 中, 因此,當需要在多台機器之間使用Forms身份認證時,就不能再使用隨機生成密鑰的方式,需要我們手工指定,保證每台機器的密鑰是一致的。
用於Forms身份認證的密鑰可以在web.config的machineKey配置節中指定,我們還可以指定加密解密算法:
decryption="Auto" [Auto | DES | 3DES | AES]
decryptionKey="AutoGenerate,IsolateApps" [String]
/>
Passport單點身份驗證:(<machineKey validationKey="5029E82E1779497186D46F83D78FAD07" decryptionKey="82B8397DB5B4443FB035083EB662CD98"
validation="SHA1" decryption="Auto" />)
1:獲取機器key 生成密鑰
2:在要實現單點登陸的項目根web.config中添加密鑰;
a) 兩個項目Web.cinfig的<machineKey> 節點確保以下幾個字段完全一樣:validationKey 、decryptionKey 、validation
b) 兩個項目的 Cookie 名稱必須相同,也就是 <forms> 中的 name 屬性,這里我們把它統一為 name ="UserLogin"
c) 注意區分大小寫
d) 登陸頁面整合到統一登陸點登陸 比如:loginUrl="www.wf.com/login.aspx", 在登陸頁面發放驗證票
在客戶端程序中訪問受限頁面
有時我們需要用代碼訪問某些頁面,比如:希望用代碼測試服務端的響應。
如果是簡單的頁面,或者頁面允許所有客戶端訪問,這樣不會有問題, 但是,如果此時我們要訪問的頁面是一個受限頁面,那么就必須也要像人工操作那樣: 先訪問登錄頁面,提交登錄數據,獲取服務端生成的登錄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處理行為,服務端也能正確識別客戶的身份。