我們前面已經討論過了如何在一個網站中集成最基本的Membership功能,然后深入學習了Membership的架構設計。正所謂從實踐從來,到實踐從去,在我們把Membership的結構吃透之后,我們要完善它,改造它,這樣我們才能真正學以致用。今天我們將以用戶信息為主線,從SqlMembershipProvider出發,到ASP.NET Simple Membership最后再到MV5中引入的ASP.NET Identity,來看看微軟是如何一步一步的改造這套框架的。
- Membership三步曲之入門篇 - Membership 基礎示例
- Membership三步曲之進階篇 - 深入剖析Provider Model
- Membership三步曲之高級篇 - 從Membership到 ASP.NET Identity
內容索引
引入 - 用戶信息是如何存在數據庫中的
我們前兩篇都只講到了怎么用Membership注冊,登錄等,但是我們漏掉了一個很重要并且是基本上每個用Membership的人都想問的,我的用戶信息怎么保存?我不可能只有用戶名和密碼,如果我要加其它的字段怎么辦?我們首先來看一下,SqlMembershipProvider是如何做的,畢竟這個Provider是跟著Membership框架一起誕生出來的。
ASP.NET 2.0時代,我們需要借助一個VS提供的一個工具來幫助我們生成所需要的表。打開VS 開發者命令行工具,輸入aspnet_regsql,后面簡單的連接一下數據庫就會幫我們生成以下的幾張表:
我們這里簡要關注以下幾張表的結構就可以了。
我想上面兩張圖應該可以說明很多問題,用戶信息的一些基本字段比如用戶名,密碼以及一些其它登錄的信息存儲在哪里,角色存儲在哪里,角色和用戶之間是如何關聯的等等,但是還有正如本節標題所說的一樣,用戶信息字段如何擴展呢?
ProfileProvider 來擴展用戶信息
我們上面講到有一張表aspnet_Profile是專門用來給ProfileProvider為擴展用戶信息的。它和MebershipProvider, RoleProvider一起組成了用戶信息,權限管理這樣一套完整的框架。下面我們就來看看如何用ProfileProvider來擴展我們想要的用戶信息。
- 我們先添加一個Model繼承ProfileBase來為我們新的用戶對象建模
- 在web.config配置ProfileProvider
- 在MVC站點中實現對我們的用戶信息的管理
UserProfile的代碼
public class UserProfile: ProfileBase { [SettingsAllowAnonymous(false)] public string FirstName { get { return base["FirstName"] as string; } set { base["FirstName"] = value; } } [SettingsAllowAnonymous(false)] public string LastName { get { return base["LastName"] as string; } set { base["LastName"] = value; } } public static UserProfile GetUserProfile(string username) { return Create(username) as UserProfile; } }
我們的UserProfile的所有字段都要從基類從獲取,基類中以object類型存儲著這些值。
web.config的配置
大家可以看到profile里面的inherits結點我們設置了我們上一步建立的那個對象,這樣我們就可以在代碼將MVC里面的Profile對象轉換成我們要的這些類型。
從Profile對象中獲取當前登錄用戶的信息
public ActionResult Manage() { var profile = Profile as UserProfile; var model = new UserProfileViewModel { FirstName = profile.FirstName, LastName = profile.LastName }; return View(model); }
保存當前用戶的信息
public ActionResult Manage(UserProfileViewModel model) { if (ModelState.IsValid) { var myProfile = Profile as UserProfile; myProfile.FirstName = model.FirstName; myProfile.LastName = model.LastName; myProfile.Save(); return RedirectToAction("Index", "Home"); } return View(model); }
怎么樣?是不是不復雜?加上我們前面學到的MembershipProvider,RoleProvider那么我們很輕松就可以將這一系列登錄、授權、認證以及用戶模塊相關的功能完成了。如果要使用ProfileProvider的話,最好是在最開始的設計階段就使用,因為要想把ProfileProvider直接集成到現有的老系統中,那是一件很難的事情,我們看一下Profile表的結構就知道了。
Profile要做到通用,那么這張表就要求能夠存儲任意類型的數據,所以微軟就采用一種這樣的設計,把所有的字段以string的格式放到了一列中,然后再解析出來。別的先不說,首先這種設計對于大型系統來說,肯定會有一個性能的瓶頸,并且如果我們想要把ProfileProvider集成到老的系統中,那會是一件很難的事情。那么微軟后面做了哪些改進呢?
Simple Membership Provider
假想一下,你使用了SQL Membership Provider,你想抱怨哪些問題呢?
- 最先抱怨的肯定是沒有辦法自定義用戶信息,必須要通過ProfileProvider,那玩意兒真心不好用!
- 其實與現有或其它系統集成簡直是太麻煩了!!
- 數據表都被你定義好了,但是很抱歉,那都不是我想要的啊!!!
- 等等。。。
好吧,這些問題確實是導致Membership一直不溫不火的原因之一。 所有這就是為什么后來,我們有了Simple Mebership Provider,借助于它:
- 我們不必再依懶于Profile Provider去擴展用戶信息。
- 可以完全讓Membership 根據我們自己定義的表結構來運行。
- 與Entity Framework集成,好吧(微軟這是捆綁銷售么? 慣用伎倆)
- 另外,在VS2012或2013中創建一個MVC4.0的Internet程序,就會為你自動添加所有代碼!
最后一招夠狠,我們來試一下。在VS2012中創建一個4.0 的MVC站點,就可以在Controllers和Models中發現相關代碼,在AccountController中已經有了登錄注冊相關的代碼。
在AccountModel中,我們可以找到一個UserProfile的類就是一個Entity Framework 的實體類。
[Table("UserProfile")] public class UserProfile { [Key] [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)] public int UserId { get; set; } public string UserName { get; set; } }
那么我們就可以像這樣查詢用戶的信息了。
var context = new UsersContext(); var username = User.Identity.Name; var user = context.UserProfiles.SingleOrDefault(u => u.UserName == username); var birthday = user.Birthday;
有人可能會問,那這個我直接用EF來整個用戶實體類做登錄模塊有啥區別? 我也懷疑區別就是可以在創建membership用戶記錄的時候,可以一起把我們的額外信息帶進去,其余的還真沒有發現什么區別。SimpleMembershipProvider所有的操作都是通過WebSecurity這個類來完成的,這個類所完成的功能與Membershipo類是一樣的,主要是對Provider的功能進行一個封裝,而這個類是包含在WebMatrix.WebData.dll中的。打開網站的引用目錄發現引用了WebMatrix.Data和WebMatrix.WebData這兩個dll。這兩個dll主要是給web page用的, 而SimpleMembershipProvider的相關代碼就包含在這兩個dll當中。
里面怎么實現的我想就不用詳述了,無非就是繼承MembershipProvider然后覆蓋其中的一些方法而已。我們Membership系列第二篇已經詳述過了,有興趣的同學請移步。在后來微軟還推出來Universal Providers,用來幫助Membership轉移到Windows Azure的以及對SQL Compact的支持。
ASP.NET Identity
基礎示例
ASP.NET Identity是在.NET Framework4.5中引入的,從Membership發布以來,我想微軟已經從開發者以及企業客戶那里面得到了足夠的反饋信息來幫助他們打造這樣一套新的框架。他所擁有的特點大多也是前面所不能滿足的,至少我們看到的是進步,不是么?
- 一套ASP.NET Identity,可以用于ASP.NET下的web form, MVC, web pages, web API等
- 和Simple Membership Provider,可以靈活訂制用戶信息,同樣采用EF Code First來完成數據操作
- 完全自定義數據結構
- 單元測試的支持
- 與Role Provider集成
- 支持面向Clamis的認證
- 支持社交賬號的登錄
- OWIN 集成
- 通過NuGet發布來實現快速迭代
瞟一眼好處還真不少,但是至少對于開發者來說,好用,能滿足需求,靈活才是王道,那我們下面就來看看如何使用ASP.NET Identity來完成我們的用戶授權和認證模塊。其實我們已經不用寫任何示例代碼,因為我們只要使用VS創建一個.NET Framework 4.5 的 MVC站點,所有的代碼都已經包括了。
默認創建的IdentityModels.cs
public class ApplicationUser : IdentityUser { } public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext() : base("DefaultConnection") { } }
我們需要在ApplicaitonUser實體中添加我們的用戶字段就可以了,同時我們還可以很簡單的更改表名。
public class ApplicationUser : IdentityUser { public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } public string City { get; set; } } public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext() : base("DefaultConnection"){} protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 默認表名是AspNetUsers,我們可以把它改成任意我們想要的 modelBuilder.Entity<IdentityUser>() .ToTable("Users"); modelBuilder.Entity<ApplicationUser>() .ToTable("Users"); } }
接下來,你就可以run一下你的網站,來體驗一把ASP.NET Identity了,別忘了先把web.config里面的連接字符串改一下,方便我們自己去查看數據庫,只要設置一下數據庫就可以了,創建工作就交給EF吧。
我們可以在AccountController中找到所有的相關代碼。
初始化UserManager對象
public AccountController() : this(new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()))) { } public AccountController(UserManager<ApplicationUser> userManager) { UserManager = userManager; } public UserManager<ApplicationUser> UserManager { get; private set; }
登錄核心代碼
var user = await UserManager.FindAsync(model.UserName, model.Password); if (user != null) { await SignInAsync(user, model.RememberMe); return RedirectToLocal(returnUrl); }
注冊核心代碼
var user = new ApplicationUser() { UserName = model.UserName }; var result = await UserManager.CreateAsync(user, model.Password);
框架設計
我們上面是直接利用VS幫助我們創建好了一些初始代碼,我們也可以創建一個空白的站點,然后再把ASP.NET Identity引用進來。所需要的類庫可以直接從Nuget上下載就可以了。
主要包括ASP.NET Identity 的EF 部分的實現,有了EF的幫助我們就可以完全自定義數據結構,當然我們也只需要定義一個實體類就可以了。
名字就已經告訴大家了,這是ASP.NET Identity的核心了,所以主要的功能在這里面。上面那個包是ASP.NET Identity EF的實現,那么我們可以在這個核心包的基礎上擴展出基于No SQL, Azure Storage 的 ASP.NET Identity實現。
ASP.NET Identity對OWIN 認證的支持。
最上面兩個就是我們自己創建的代碼,分別繼承自己Microsoft.AspNet.Identity.EntityFramework的IdentityUser和IdentityDbContext。但是最后別忘了,我們與用戶相關的操作實際上是通過Microsoft.AspNet.Identity.Core的 UserManager類來完成的。通過這樣一種設計,可以把具體定義和實現交給上層,但是最后的核心卻完全由自己掌控,實現松耦合,高內聚(一不小心我竟然說出了這么專業的解釋,小心臟砰砰跳呀!)。
框架實現剖析
上面只是一張粗略的類圖,下面我們就來看一下這些類之間是如何關聯起來協作的。我們通過上面基礎示例的代碼可以發現,用用戶相關的功能是通過調用UserManager的方法來完成的。 我們可以在AccountController中找到UserManager的初始代碼:
new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));
雖然所說有的方法通過UserManager來調用,但是最后實現的還是UserStore,并且如果我們找到UserManager的定義,會發現實際上它所接收的正是在Microsoft.AspNet.Identity.Core中定義的IUserStore接口。
public UserManager(IUserStore<TUser> store) { this.Store = store; }
我們現在使用的是ASP.NET Identity EF的實現,所以在UserStore中,直接調用傳進來的DbContext的Save操作就可以了。
有沒有發現這張圖和我們第二篇中講的Provider模式有那么點點的神似? 在Membership中,我們所有的操作通過調用Membership來過多成,但是Membership本身只是一個包裝類,內部的操作實際上是通過Provider的實際類來完成的,這就是策略模式的典型案例。只不過Membership的Provider通過web.config配置完成,而UserManager通過構造函數注入完成。
擴展ASP.NET Identity - 將用戶信息寫入文件
為了熟悉AspNet.Identity的結構,我們來擴展實現一個將用戶信息寫入文件的組件,然后實現登錄注冊功能,我們就給它命名AspNet.Identity.File吧。
- 創建一個自己的用戶類(UserIdentity)實現Microsoft.AspNet.Identity.IUser接口
- 創建一個自己的UserStore類實現Microsoft.AspNet.Identity.IUserStore<TUser>接口
- 作為演示,我們的用戶類就盡量簡單,只有id,用戶名,和密碼三個屬性
- 我們的UserStore,也只重寫了Get和Create幾個基本的方法,沒有重寫Update。
UserIdentity.cs 代碼
public class IdentityUser : IUser { public string Id { get; set; } public string UserName { get; set;} public string PasswordHash{ get; set; } public override string ToString() { return string.Format("{0},{1},{2}", this.Id, this.UserName, this.PasswordHash); } public static IdentityUser FromString(string strUser) { if (string.IsNullOrWhiteSpace(strUser)) { throw new ArgumentNullException("user"); } var arr = strUser.Split(','); if (arr.Length != 3) { throw new InvalidOperationException("user is not valid"); } var user = new IdentityUser(); user.Id = arr[0]; user.UserName = arr[1]; user.PasswordHash = arr[2]; return user; } }
UserStore.cs的核心代碼
// 創建用戶 public async Task CreateAsync(IdentityUser user) { user.Id = Guid.NewGuid().ToString(); using (var stream = new IO.StreamWriter(_filePath, true, Encoding.UTF8)) { await stream.WriteLineAsync(user.ToString()); } } // 根據用戶名找用戶 public async Task<IdentityUser> FindByNameAsync(string userName) { using (var stream = new IO.StreamReader(_filePath)) { string line; IdentityUser result = null; while ((line = await stream.ReadLineAsync()) != null) { var user = IdentityUser.FromString(line); if (user.UserName == userName) { result = user; break; } } return result; } }
AccountController.cs核心代碼
// 初始化 UserManager public AccountController() : this(new UserManager<IdentityUser>(new UserStore(System.Web.HttpContext.Current.Server.MapPath("~/App_Data/user.txt")))) { } // 檢查用用戶名密碼是否正確 var user = await UserManager.FindAsync(model.UserName, model.Password); if (user != null) { // Forms 登錄代碼 } // 注冊用戶 var user = new IdentityUser() { UserName = model.UserName }; var result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { // 創建用戶成功 }
保存到txt中的用戶信息
小結
Membership系列這三篇,從入門到精通到這里就算是結束了,不知道能不能算是園滿。因為這三篇的關注度都不是很高,可能沒有從多少人在乎這個玩意。不過還是要感謝@好玩一人的催促,讓我堅持把這三篇寫完了。可能Membership不是.NET里面非常成功的一部份,但是這并不能說它不好,而是因為像這種需求的東西如果要做成類庫本身就是一項比較困難的事情,因為幾乎很少有一模一樣的需求。
但是我們更應該關注的是微軟是如何面對復雜多變的需求來設計框架的,如何從一大堆的零散需求中找出最核心的部份, 他們如何解耦,如何提高可擴展性和維護性的。從Membersihp引入.NET的時候給我們帶來了Provider,于是我們會發現.NET2.0開始就出現了各種Provider,web.config里面各種配置。而最新的ASP.NET Identity已經不再用那樣的Provider模式了,但是思想卻大致相同,只不過換成了用范型來實現,用構造函數注入,這也是從MVC以來微軟框架的一些特色。而我們,在追求微軟技術的同時,更應該理解其內在的一些思想和本質,這樣才不致于被淹沒在無盡的新技術中,因為很多其實只是換湯不換藥,或者我們可以用積極的話來說,微軟在不斷的提高開發人員的效率,并且讓你寫代碼的時候有更好的心情。 請相信我,理解了本質,再去學習新技術,能讓你效率翻倍。
最后,還是謝謝大家一直的關注和陪伴。
下面的demo的鏈接下載,包括一個ProfileProvider的例子,和后面將用戶信息寫入txt文件的例子。
AspNet.Identity.File: http://pan.baidu.com/s/1dD5SZ1v
ProfileProvider Demo: http://pan.baidu.com/s/1bnnakZt
文章列表