在之前的文章中,我為大家介紹了OWIN和Katana,有了對它們的基本瞭解後,才能更好的去學習ASP.NET Identity,因為它已經對OWIN 有了良好的整合。
在這篇文章中,我主要關注ASP.NET Identity的建立和使用,包括基礎類的搭建和使用者管理功能的實現——
在後續文章中,我將探索它更高階的用法,比如身份驗證並聯合ASP.NET MVC 進行授權、使用第三方登入、宣告式認證等。
ASP.NET Identity 前世今生
ASP.NET Membership
在ASP.NET 2.0時代,ASP.NET Membership用於使用者管理的常見需求。包括表單身份驗證(Form Authentication),一個用於儲存使用者名稱、密碼和其他使用者資訊的 SQL Server 資料庫。但是現在,對於 Web 應用程式的資料儲存我們有了更多的選擇。而且,大多數開發者希望自己的站點能夠使用第三方供應商提供的社交賬號來實現身份驗證和授權。但是,由於 ASP.NET Membership自身設計的限制,已經難以滿足如下變化:
- 資料庫架構為 SQL Server 設計,而且無法修改。雖然你可以新增額外的使用者資訊,但這些資料被存入了一張不同的資料表。而且這些資訊難以訪問,除了使用 Profile Provider API。
- 雖然通過Provider,你可以對後臺資料儲存結構的修改,但是該Provider的設計是假設我們對關係型資料庫進行修改。雖然你也可以寫一個面向非關係型(例如 Windows Azure Tables)儲存機制的Provider。但是,圍繞著相關的設計,你還需要大量的工作。這包括編寫大量的程式碼,以及為那些 NoSQL 資料庫不支援的方法丟擲一大堆 System.NotImplementedException 異常。
- 由於登入、登出功能基於表單身份驗證,因此ASP.NET Membership 無法支援 OWIN。OWIN 包括了一些用於身份驗證的 Middleware 中介軟體,如支援Microsoft 賬戶、 Facebook,、Google、Twitter 等的登入,還支援來自於組織內部的賬號例如 Active Directory 、 Windows Azure Active Directory 等登入。OWIN 也提供了包括對OAuth 2.0, JWT 和CORS的支援。
正是由於ASP.NET Membership 諸多限制,微軟採取了一系列的補救措施,比如釋出了ASP.NET Simple Membership 和ASP.NET Universal Providers,他們通過Entity Framework的Code First,可以方便的去擴充套件使用者資訊,而非像ASP.NET Membership 那樣需要Provider 來實現。
但是它們仍舊存在不足,主要包括如下兩點:
- 對非關係型資料庫支援不好
- 無法和OWIN相容
ASP.NET Identity
由於ASP.NET Membership、ASP.NET Simple Membership 、ASP.NET Universal Providers 設計上的不足,微軟在接受了大量反饋後,於.NET Framework 4.5 中推出了ASP.NET Identity,如果用一句話概括——ASP.NET Identity 為ASP.NET 應用程式提供了一系列的API用來管理和維護使用者 ,它包括如下新特性:
• One ASP.NET Identity
- ASP.NET Identity 可以用在所有的 ASP.NET 框架上,例如 ASP.NET MVC, Web Forms,Web Pages,ASP.NET Web API 和SignalR
- ASP.NET Identity 可以用在各種應用程式中,例如Web 應用程式、移動應用、商店應用或者混合架構應用
• 易於管理使用者資訊
- ASP.NET Identity提供了豐富的API ,可以方便的管理使用者
• 持久化控制
- 預設情況下,ASP.NET Identity將使用者所有的資料儲存在資料庫中。ASP.NET Identity 使用 Entity Framework 實現其所有的檢索和持久化機制。
- 通過Code First,你可以對資料庫架構的完全控制,一些常見的任務例如改變表名稱、改變主鍵資料型別等都可以很輕易地完成。
- 能夠很容易地引入其他不同的儲存機制,例如 SharePoint, Windows Azure 儲存表服務, NoSQL 資料庫等。不必再丟擲 System.NotImplementedException 異常了。
• 單元測試能力
- ASP.NET Identity 能讓 Web 應用程式能夠更好地進行單元測試。你可以為你應用程式使用了 ASP.NET Identity 的部分編寫單元測試。
• 角色Provider
- ASP.NET Identity 中的角色Provider配合ASP.NET MVC Authorize,可以讓你基於角色來限制對應用程式某個部分的訪問。你可以很容易地建立Admin之類的角色,並將使用者加入其中。
• 基於宣告的
- ASP.NET Identity 支援基於宣告的身份驗證,它使用一組”宣告”來表示使用者的身份標識。相對於”角色”,”宣告”能使開發人員能夠更好地描述使用者的身份標識。”角色”本質上只是一個布林型別(即”屬於”或”不屬於”特定角色),而一個”宣告”可以包含更多關於使用者標識和成員資格的資訊。
• 社交賬號登入Provider
- 你可以很容易的為你的應用程式加入社交賬號登入功能(例如 Microsoft 賬戶,Facebook,,Twitter,Google 等),並將使用者特定的資料存入你的應用程式。
• Windows Azure Active Directory
- 你還可以加入使用 Windows Azure Active Directory 進行登入的功能,並將使用者特定的資料存入你的應用程式。
• OWIN 整合
- ASP.NET 身份驗證現在是基於 OWIN 中介軟體實現,並且可以在任何基於 OWIN 的宿主上使用。ASP.NET Identity 不依賴System.Web程式集,與此同時,它完全相容於 OWIN 框架,並且能被用在任何基於OWIN 的Host和Server 之上。
- ASP.NET Identity使用OWIN Authentication來登入、登出操作。這意味著應用程式使用CookieAuthentication 生成 cookie 而非FormsAuthentication 。
• NuGet 包
- ASP.NET Identity 作為一個 NuGet 包進行釋出,並且安裝在ASP.NET MVC,Web Forms 和 ASP.NET Web API 專案模板中。當然,你也可以從 NuGet 庫中下載它。
- ASP.NET Identity以NuGet包的形式釋出,這樣能讓ASP.NET 團隊更好的Bug修復和迭代新功能,與此同時,開發人員可以在第一時間獲取到最新版本。
建立 ASP.NET Identity
建立 ASP.NET Identity資料庫
ASP.NET Identity並不像ASP.NET Membership那樣依賴SQL Server架構,但關係型儲存仍然是預設和最簡單的實現方式,儘管近些年來NoSQL發展迅猛,但關係型資料庫易於理解,仍舊是開發團隊內部主流的儲存選擇。
ASP.NET Identity使用Entity Framework Code First來自動建立資料庫架構。在此示例中,我使用localdb來建立一個空的資料庫IdentityDb,然後交由Code First管理資料庫架構。
localdb內建在Visual Studio中而且它是輕量級的SQL Server,能讓開發者簡單快速運算元據庫。
新增ASP.NET Identity 包
Identity以包的形式釋出在NuGet上,這能夠很方便的將它安裝到任意專案中,通過在Package Manger Console輸入如下命令來安裝Identity:
- Install-Package Microsoft.AspNet.Identity.EntityFramework
- Install-Package Microsoft.AspNet.Identity.OWIN
- Install-Package Microsoft.Owin.Host.SystemWeb
在 Visual Studio中選擇建立一個完整的ASP.NET MVC專案時,預設情況下該模板會使用ASP.NET Identity API自動新增通用的使用者管理模組。對於初學者,我建議學習它裡面API的使用,但我不推薦將它使用在正式環境中,因為它產生了過多的通用和冗餘程式碼,有時候我們只想讓它簡單工作。
更新Web.config檔案
若要將ASP.NET Identity使用在專案裡,除了新增相應的包之外,還需要在Web.config中新增如下配置資訊:
- 資料庫連線字串
- 指定的OWIN Startup啟動項,用作初始化Middleware至Pipeline
1 2 3 4 5 6 7 |
<connectionStrings> <add name="IdentityDb" providerName="System.Data.SqlClient" connectionString="Data Source=(localdb)\v11.0;Initial Catalog=IdentityDb;Integrated Security=True;Connect Timeout=15;Encrypt=False;TrustServerCertificate=False; MultipleActiveResultSets=True" /> </connectionStrings> <appSettings> <add key="owin:AppStartup" value="UsersManagement.IdentityConfig" /> </appSettings> |
建立Entity Framework 類
如果大家使用過ASP.NET Membership,對比過後你會發現在ASP.NET Identity擴充套件User資訊是多麼的簡單和方便。
1.建立 User 類
第一個要被建立的類它代表使用者,我將它命名為AppUser,繼承自Microsoft.AspNet.Identity.EntityFramework 名稱空間下IdentityUser,IdentityUser 提供了基本的使用者資訊,如Email、PasswordHash、UserName、PhoneNumber、Roles等,當然我們也可以在其派生類中新增額外的資訊,程式碼如下:
1 2 3 4 5 6 7 8 |
using Microsoft.AspNet.Identity.EntityFramework; namespace UsersManagement.Models { public class AppUser:IdentityUser { } } |
2.建立 Database Context 類
接下來的步驟就是建立EF Database Context 來操作AppUser。ASP.NET Identity將使用Code First 來建立和管理資料庫架構。值得注意的是,Database Context必須繼承自IdentityDbContext<T>,而且T為User類(在此示例即AppUser),程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public class AppIdentityDbContext : IdentityDbContext<AppUser> { public AppIdentityDbContext() : base("IdentityDb") { } static AppIdentityDbContext() { Database.SetInitializer<AppIdentityDbContext>(new IdentityDbInit()); } public static AppIdentityDbContext Create() { return new AppIdentityDbContext(); } } public class IdentityDbInit : DropCreateDatabaseIfModelChanges<AppIdentityDbContext> { protected override void Seed(AppIdentityDbContext context) { PerformInitialSetup(context); base.Seed(context); } public void PerformInitialSetup(AppIdentityDbContext context) { //初始化 } } |
上述程式碼中,AppIdentityDbContext 的建構函式呼叫基類建構函式並將資料庫連線字串的Name作為引數傳遞,它將用作連線資料庫。同時,當Entity Framework Code First成功建立資料庫架構後,AppIdentityDbContext的靜態建構函式呼叫Database.SetInitializer方法Seed 資料庫而且只執行一次。在這兒,我的Seed 類IdentityDbInit。
最後,AppIdentityDbContext 定義了 Create方法,它將被 OWIN Middleware回掉然後返回AppIdentityDbContext例項,這個例項被儲存在OwinContext中。
3.建立User Manger 類
User Manager類作為ASP.NET Identity中最為重要的類之一,用來管理User。同樣,自定義的User Manger類必須繼承自UserManager<T >,此處T就為AppUser。UserManager<T>提供了建立和操作使用者的一些基本方法並且全面支援C# 非同步程式設計,所以你可以使用CreateAsync(Create),FindAsync(Find)、DeleteAsync(Delete)、UpdateAsync(Update)來進行使用者管理,值得注意的是,它並不通過Entity Framework 來直接操作使用者,而是間接呼叫UserStore來實現。UserStore<T>是Entity Framework 類並實現了IUserStore<T>介面,並且實現了定義在UserManger中操作使用者的方法。程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/// <summary> /// 使用者管理 /// </summary> public class AppUserManager : UserManager<AppUser> { public AppUserManager(IUserStore<AppUser> store) : base(store) { } public static AppUserManager Create( IdentityFactoryOptions<AppUserManager> options, IOwinContext context) { AppIdentityDbContext db = context.Get<AppIdentityDbContext>(); //UserStore<T> 是 包含在 Microsoft.AspNet.Identity.EntityFramework 中,它實現了 UserManger 類中與使用者操作相關的方法。 //也就是說UserStore<T>類中的方法(諸如:FindById、FindByNameAsync...)通過EntityFramework檢索和持久化UserInfo到資料庫中 AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db)); return manager; } } |
上述程式碼中,靜態的Create方法將返回AppUserManger例項,它用來操作和管理使用者,值得注意的是,它需要傳入OwinContext物件,通過該上下文物件,獲取到儲存在Owin環境字典中的Database Context例項。
4.建立OWIN Startup 類
最後,通過Katana(OWIN的實現)提供的API,將Middleware 中介軟體註冊到Middleware中,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class IdentityConfig { public void Configuration(IAppBuilder app) { //1.使用app.Use方法將IdentityFactoryMiddleware和引數callback回掉函式註冊到Owin Pipeline中 //app.Use(typeof(IdentityFactoryMiddleware<T, IdentityFactoryOptions<T>>), args); //2.當IdentityFactoryMiddleware中介軟體被Invoke執行時,執行callback回掉函式,返回具體例項Instance //TResult instance = ((IdentityFactoryMiddleware<TResult, TOptions>) this).Options.Provider.Create(((IdentityFactoryMiddleware<TResult, TOptions>) this).Options, context); //3.將返回的例項儲存在Owin Context中 //context.Set<TResult>(instance); app.CreatePerOwinContext<AppIdentityDbContext>(AppIdentityDbContext.Create); app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create); app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), }); } } |
上述程式碼中,通過CreatePerOwinContext方法將AppIdentityDbContext和 AppUserManager的例項註冊到OwinContext中,這樣確保每一次請求都能獲取到相關ASP.NET Identity物件,而且還能保證全域性唯一。
UseCookieAuthentication 方法指定了身份驗證型別為ApplicationCookie,同時指定LoginPath屬性,當Http請求內容認證不通過時重定向到指定的URL。
使用ASP.NET Identity
成功建立ASP.NET Identity之後,接下來就是如何去使用它了,讓我們再回顧一下ASP.NET Identity的幾個重要知識點:
- 大多數應用程式需要使用者、角色管理,ASP.NET Identity提供了API用來管理使用者和身份驗證
- ASP.NET Identity 可以運用到多種場景中,通過對使用者、角色的管理,可以聯合ASP.NET MVC Authorize 過濾器 來實現授權功能。
獲取所有的Users物件
在上一小節中,通過CreatePerOwinContext方法將AppIdentityDbContext和 AppUserManager的例項註冊到OwinContext中,我們可以通過OwinContext物件的Get方法來獲取到他們,將下面程式碼放在Controller中,方便供Action獲取物件:
1 2 3 4 |
private AppUserManager UserManager { get { return HttpContext.GetOwinContext().GetUserManager<AppUserManager>(); } } |
在上述程式碼中,通過Microsoft.Owin.Host.SystemWeb 程式集,為HttpContext增加了擴充套件方法GetOwinContext,返回的 OwinContext物件是對Http請求的封裝,所以GetOwinContext方法可以獲取到每一次Http請求的內容。接著通過IOwinContext的擴充套件方法GetUserManager獲取到儲存在OwinContext中的UserManager例項。
然後,通過UserManager的Users屬性,可以獲取到所有的User集合,如下所示:
1 2 3 4 |
public ActionResult Index() { return View(UserManager.Users); } |
建立User物件
通過UserManager的CreateAsync方法,可以快速的建立User物件,如下程式碼建立了User ViewModel:
1 2 3 4 5 6 7 8 9 |
public class UserViewModel { [Required] public string Name { get; set; } [Required] public string Email { get; set; } [Required] public string Password { get; set; } } |
使用UserManager物件的CreateAsync方法將AppUser物件將它持久化到資料庫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[HttpPost] public async Task<ActionResult> Create(UserViewModel model) { if (ModelState.IsValid) { var user = new AppUser {UserName = model.Name, Email = model.Email}; //傳入Password並轉換成PasswordHash IdentityResult result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { return RedirectToAction("Index"); } AddErrorsFromResult(result); } return View(model); } |
CreateAsync返回IdentityResult 型別物件,它包含如下了兩個重要屬性:
- Succeeded : 如果操作成功返回True
- Errors:返回一個字串型別的錯誤集合
通過AddErrorsFromResult 方法將錯誤集合展示在頁面上 @Html.ValidationSummary 處,如下所示:
1 2 3 4 5 6 7 |
private void AddErrorsFromResult(IdentityResult result) { foreach (string error in result.Errors) { ModelState.AddModelError("", error); } } |
新增自定義密碼驗證策略
有時候,我們需要實現密碼策略,如同AD中控制那樣,密碼複雜度越高,那麼它被破譯的概率就越低。
ASP.NET Identity 提供了PasswordValidator類,提供瞭如下屬性來配置密碼策略:
RequiredLength | 指定有效的密碼最小長度 |
RequireNonLetterOrDigit | 當為True時,有效的密碼必須包含一個字元,它既不是數字也不是字母 |
RequireDigit | 當為True時,有效密碼必須包含數字 |
RequireLowercase | 當為True時,有效密碼必須包含一個小寫字元 |
RequireUppercase | 當為True時,有效密碼必須包含一個大寫字元 |
如果這些預定義屬性無法滿足我們的需求時,我們可以新增自定義的密碼驗證策略,只要繼承PasswordValidator 並且Override ValidateAsync方法即可,如下程式碼所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class CustomPasswordValidator : PasswordValidator { public override async Task<IdentityResult> ValidateAsync(string password) { IdentityResult result = await base.ValidateAsync(password); if (password.Contains("12345")) { List<string> errors = result.Errors.ToList(); errors.Add("密碼不能包含連續數字"); result = new IdentityResult(errors); } return result; } } |
上述程式碼中,值得注意的是,IdentityResult 物件的 Errors是隻讀的,所以無法直接賦值,只能通過例項化IdentityResult 類並通過建構函式傳入Errors。
自定義的密碼策略建立完畢過後,接著就將它附加到UserManager物件的PasswordValidator 屬性上,如下程式碼所示:
1 2 3 4 5 6 7 8 9 |
//自定義的Password Validator manager.PasswordValidator = new CustomPasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = false, RequireDigit = false, RequireLowercase = true, RequireUppercase = true }; |
更多使用者驗證策略
UserManager 除了PasswordValidator之外,還提供了一個更加通用的屬性:UserValidator ,它包含如下兩個策略屬性:
AllowOnlyAlphanumericUserNames | 當為True時,UserName只能包含字母數字 |
RequireUniqueEmail | 當為True時,Email地址必須唯一 |
當然這兩種策略如果不滿足我們的需求的話,我們也可以像Password那樣去定製化,只要 繼承UserValidator<T> 然後 Override ValidateAsync 方法,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class CustomUserValidator : UserValidator<AppUser> { public CustomUserValidator(AppUserManager mgr) : base(mgr) { } public override async Task<IdentityResult> ValidateAsync(AppUser user) { IdentityResult result = await base.ValidateAsync(user); if (!user.Email.ToLower().EndsWith("@jkxy.com")) { List<string> errors = result.Errors.ToList(); errors.Add("Email 地址只支援jkxy域名"); result = new IdentityResult(errors); } return result; } } |
上述程式碼增強了對Email的驗證,必須為@jkxy域名,然後將自定義的UserValidator 附加到User Manger 物件上:
1 2 3 4 5 |
//自定義的User Validator manager.UserValidator = new CustomUserValidator(manager) { AllowOnlyAlphanumericUserNames = true, RequireUniqueEmail = true }; |
ASP.NET Identity 其他API介紹
在上一小節中,介紹了CreateAsync 的使用,接下來一鼓作氣,繼續ASP.NET Identity之旅。
實現Delete 使用者功能
按照我們的經驗,若要刪除一個使用者,首先需要Find 它。通過UserManager 物件的 FindByIdAsync來找到要被刪除的物件,如果該物件不為null,那麼再呼叫UserManager物件的DeleteAsync來刪除它,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[HttpPost] public async Task<ActionResult> Delete(string id) { AppUser user = await UserManager.FindByIdAsync(id); if (user != null) { IdentityResult result = await UserManager.DeleteAsync(user); if (result.Succeeded) { return RedirectToAction("Index"); } return View("Error", result.Errors); } return View("Error", new[] {"User Not Found"}); } |
實現編輯使用者操作
因為編輯操作UpdateAsync 只接受一個引數,而不像CreateAsync那樣可以傳入Password,所以我們需要手動的去校驗並給PasswordHash屬性賦值,當密碼策略驗證通過時再去驗證Email策略,這樣確保沒有髒資料,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
[HttpPost] public async Task<ActionResult> Edit(string id, string email, string password) { //根據Id找到AppUser物件 AppUser user = await UserManager.FindByIdAsync(id); if (user != null) { IdentityResult validPass = null; if (password != string.Empty) { //驗證密碼是否滿足要求 validPass = await UserManager.PasswordValidator.ValidateAsync(password); if (validPass.Succeeded) { user.PasswordHash = UserManager.PasswordHasher.HashPassword(password); } else { AddErrorsFromResult(validPass); } } //驗證Email是否滿足要求 user.Email = email; IdentityResult validEmail = await UserManager.UserValidator.ValidateAsync(user); if (!validEmail.Succeeded) { AddErrorsFromResult(validEmail); } if ((validEmail.Succeeded && validPass == null) || (validEmail.Succeeded && validPass.Succeeded)) { IdentityResult result = await UserManager.UpdateAsync(user); if (result.Succeeded) { return RedirectToAction("Index"); } AddErrorsFromResult(result); } } else { ModelState.AddModelError("", "無法找到改使用者"); } return View(user); } |
小節
在這篇文章中,我為大家介紹了什麼是ASP.NET Identity以及怎樣配置和建立它的基礎類,然後演示使用API 進行使用者的管理。在下一篇文章中,繼續ASP.NET Identity之旅,探索身份驗證和授權的使用,謝謝 。