一個完整的ASP.NET的請求中會存在身份驗證(Authentication)階段以及授權(Authorization)階段,英文單詞Authentication和Authorization非常相似,所以很多時候會混淆這兩個概念。身份驗證(Authentication)的目的是知道“你”是誰,而授權(Authorization)則是當“你”訪問一個資源時是否符合訪問條件,符合就將訪問權限授權給你進行訪問,否則拒絕訪問。
本文將從以下幾點介紹ASP.NET MVC如何使用Identity完成資源訪問的限制:
● 資源訪問的限制方式
● ASP.NET中的訪問限制
● ASP.NET MVC中基的訪問限制
● ASP.NET MVC中的用戶信息
● ASP.NET Identity用戶身份信息填充
● ASP.NET MVC訪問限制的實現
● ASP.NET MVC基於用戶聲明的訪問限制及自定義限制
資源訪問的限制方式
什么是資源?在Web中通過URI(Uniform Resource Identifier,統一資源標識符)來對HTML文檔、圖片、圖像等內容定位,反過來說在Web中HTML文檔、圖片、圖像等內容就是資源,而資源訪問的限制就是對在用戶通過URI訪問Web資源時,判斷該用戶是否有權限訪問該資源,如果有則繼續訪問,否則拒絕訪問。
資源訪問有以下幾種限制方式:
● 匿名訪問限制:所有人都可以訪問資源。
● 根據用戶名訪問限制:指定特定的用戶,讓其能夠或者不能訪問資源。
● 根據用戶角色訪問限制:指定特定角色,讓擁有該角色的用戶能夠訪問資源。
● 根據用戶聲明(Claim)訪問限制:指定特定的聲明(Claim),讓身份信息中含有該聲明的用戶能夠訪問資源。
● 使用其它用戶信息進行訪問限制:根據用戶身份的其它信息來判斷用戶是否能夠訪問資源。
從上面幾點可以看出資源訪問的限制或者說授權,實際上就是通過用戶信息來判斷用戶是否有訪問資源的權限,而常用的信息是用戶名、用戶角色以及用戶聲明。
注:對於授權來說,它處於身份驗證的后續階段,所以可以認為在授權階段時已經存在用戶信息,所以可以直接使用用戶信息來完成訪問限制。
ASP.NET中的訪問限制
ASP.NET中通過HTTPModule的方式實現了FileAuthorizationModule以及UrlAuthorizationModule來對用戶訪問文件以及其它資源進行權限控制,其中UrlAuthorizationModule可以通過在web.config中添加如下配置來通過用戶名或者用戶角色限制訪問:
在ASP.NET中可以通過Forms驗證+UrlAuthorizationModule來實現用戶身份驗證和訪問授權,更多信息可參考文檔:https://docs.microsoft.com/en-us/aspnet/web-forms/overview/older-versions-security/membership/user-based-authorization-cs
ASP.NET MVC中的訪問限制
Forms驗證+UrlAuthorizationModule的方式是用於基於ASP.NET Web Form的應用程序,而ASP.NET MVC雖然也可以使用Forms驗證,但是ASP.NET MVC的授權方式是不一樣的,它是通過過濾器的方式實現,下面代碼為之前文章中用於限制后台管理頁面需要登錄的代碼:
通過在Controller上使用了一個名為Authorize的特性來實現的,這個特性的定義如下:
它用於當用戶訪問Controller或Action方法時可以通過用戶信息對其訪問進行限制。
在Authorize特性的定義中可以看到名為Roles以及Users的屬性,其作用就是設置可以訪問該資源的用戶或者角色:
使用方法如下所示:
ASP.NET MVC中的用戶信息
通過前面的介紹可以知道用戶的授權是根據用戶信息來的,無論是基於角色的、用戶的、聲明的甚至是自定義的,都需要依賴用戶信息進行權限判斷,那么ASP.NET MVC中到底包含什么用戶信息?
1. HttpContextBase與IPrincipal:
首先可以知道的是在ASP.NET中有一個最核心的HTTP上下文對象HttpContextBase,它保存了整個Http請求到響應過程的所有相關數據,其定義如下:
其中就包含了一個名為User的IPrincipal類型:
該類型中的Identity屬性就包含了用戶的信息:
從接口中可以看到它僅僅包含了用戶名、身份驗證反射以及是否驗證通過三個屬性。
注:IPrincipal的實現有多種而本例中使用的是ClaimPrincipal。
2. ClaimsIdentity與AuthenticationTicket:
通過前面的文章分析得知Identity基於Cookie的身份驗證方式實際上是對一個AuthenticationTicket對象序列化加密、反序列化解密的過程,而這個AuthenticationTicket就攜帶了所有用戶的信息,在AuthenticationTicket的定義中可以看到兩個重要的對象,其中AuthenticationProperties保存了身份驗證的會話信息,如過期時間、是否允許刷新等。而另一個ClaimsIdentity屬性就是以聲明(Claim)的方式實現的用戶信息。
ClaimsIdentity的部分定義如下:
其中除了實現IIdentity接口的屬性外,還有一個重要的屬性是Claims,它用於以聲明的方式來保存用戶信息,那么Identity是如何完成用戶數據填充的?
ASP.NET Identity用戶身份信息填充
用戶的獲取填充主要是在用戶登錄(注冊用戶后會自動登錄)的時候完成的,因為在后續的請求中Identity僅需通過解析加密后的用戶信息字符串即可獲得用戶信息(注:會存在重新生成刷新該信息的情況,如身份信息的滑動過期等)。
通過前面文章的分析知道了在Identity中用戶的登錄是通過SignInManager對象完成的以下是用戶登錄的代碼及注冊代碼:
以下是注冊代碼,實際上是創建完成用戶后執行了登錄操作:
注:通過對源碼分析,SignInManager.PasswordSignInAsync方法實際上最后也是調用SignInAsync方法完成的登錄。
那么SignInAsync到底做了什么?
從代碼中可以看到,該方法調用了一個名為CreateUserIdentityAsync的方法,根據其方法名、參數以及返回值類型來看就已經可以確定該方法就是通過用戶對象生成上面提到的用於以聲明的方式保存用戶信息的方法。從它的實現中可以看出它實際上是通過UserManager生成的:
而UserManager又是通過ClaimsIdentityFactory完成的:
ClaimsIdentityFactory.CreateAsync方法的實現:
注:實際上身份信息的刷新也是通過UserManager完成的:
方法的實現仍然是UserManager的CreateIdentityAsync方法:
從實現中可以看出如果UserManager支持角色、聲明等功能,它會從數據庫中加載對應的信息以聲明(Claim)的形式保存在ClaimsIdentity對象中。
在數據庫中添加以下數據(為了演示功能直接在數據庫中添加角色、聲明信息並與用戶數據進行關聯,如果要開發此功能可基於UserManager完成):
角色信息:
用戶聲明(Claim):
角色與用戶信息關聯:
注:由於此處Identity EF與MySQL,對象與表映射存在問題,所以多了一些ID列,暫時不管這個問題,關於Identity與MySQL用法可以參考這篇文檔:https://docs.microsoft.com/en-us/aspnet/identity/overview/getting-started/aspnet-identity-using-mysql-storage-with-an-entityframework-mysql-provider
運行程序並登錄后,在用戶信息(ClaimsIdentity)中可找到添加的角色和聲明信息:
ASP.NET MVC訪問限制的實現
上面介紹了用戶信息的填充,那么訪問的限制實際上就是對用戶信息比較而已,下面是Authorize特性的核心方法:
其中核心的三個判斷為:
● user.Identity.IsAuthenticated:必須通過身份驗證。
● (this._usersSplit.Length <= 0 || this._usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase)):特性沒有指定用戶或者當前用戶存在於指定的用戶列表中。
● (this._rolesSplit.Length <= 0 || this._rolesSplit.Any(new Func<string, bool>(user.IsInRole)):特性沒有指定角色或者當前用戶擁有指定的角色。
以上三個條件必須全部符合才能夠訪問。
ASP.NET MVC基於用戶聲明的訪問限制及自定義訪問限制
ASP.NET MVC中雖然用戶信息是基於聲明的方式保存的,但是卻沒有實際的實現,所以需要自己動手實現一個(注:也可以參考ASP.NET Core中的實現https://docs.microsoft.com/en-us/aspnet/core/security/authorization/claims)。
實現一個自定義的授權特性(注:之前分析過HttpContext中的User是一個IPrincipal類型,實際上MVC使用的是與ClaimsIdentity對應的ClaimsPrincipal)代碼如下,該過濾器只是在原有的授權方式基礎上添加了聲明的檢查:
使用方式如下:
登錄后訪問上面action得到下面結果,驗證通過:
注:需要注意的是以上介紹的授權方式,無論是通過角色、用戶名還是聲明,它都需要以硬編碼的形式寫在代碼中,換句話說就是在開發時必須確定該功能或者Controller/Action訪問需求。但是一些時候也會出現訪問需求不確定的情況,訪問權限的配置會在運行時通過配置文件或者數據庫來動態配置,這樣的話自定義的授權過濾器就需要依賴一些業務組件來實現自定義的授權流程。
小結
本章介紹了授權與身份驗證的關系以及在ASP.NET中的實現,並詳細介紹了ASP.NET MVC中的Identity是如何使用身份驗證數據來完成授權的。常見的授權方式一般是基於用戶名、角色以及聲明,但是它們使用的場景邊界是不那么明確的,就是說用什么都行實際情況需要根據需求來看,一般權限控制較簡單的使用基於角色的授權即可。但無論基於什么來對用戶授權,這些信息都屬於用戶信息,所以在拓展用戶的授權時首先要考慮的是用戶的特征信息,其次是用戶身份驗證時如何獲取填充這些信息,最后才是考慮如何使用這些信息來進行授權。
參考:
https://msdn.microsoft.com/en-us/library/wce3kxhd.aspx
https://stackoverflow.com/questions/21645323/what-is-the-claims-in-asp-net-identity
https://www.codeproject.com/Articles/98950/ASP-NET-authentication-and-authorization