我們前面已經討論過了如何在一個網站中整合最基本的Membership功能,然後深入學習了Membership的架構設計。正所謂從實踐從來,到實踐從去,在我們把Membership的結構吃透之後,我們要完善它,改造它,這樣我們才能真正學以致用。今天我們將以使用者資訊為主線,從SqlMembershipProvider出發,到ASP.NET Simple Membership最後再到MV5中引入的ASP.NET Identity,來看看微軟是如何一步一步的改造這套框架的。
內容索引
- 引入 – 使用者資訊是如何存在資料庫中的?
- ProfileProvider來擴充套件使用者資訊
- Simple Membership Provider
- 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的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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物件中獲取當前登入使用者的資訊
1 2 3 4 5 6 7 8 9 10 |
public ActionResult Manage() { var profile = Profile as UserProfile; var model = new UserProfileViewModel { FirstName = profile.FirstName, LastName = profile.LastName }; return View(model); } |
儲存當前使用者的資訊
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 的實體類。
1 2 3 4 5 6 7 8 |
[Table("UserProfile")] public class UserProfile { [Key] [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)] public int UserId { get; set; } public string UserName { get; set; } } |
那麼我們就可以像這樣查詢使用者的資訊了。
1 2 3 4 |
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
1 2 3 4 5 6 7 8 9 |
public class ApplicationUser : IdentityUser { } public class ApplicationDbContext : IdentityDbContext { public ApplicationDbContext() : base("DefaultConnection") { } } |
我們需要在ApplicaitonUser實體中新增我們的使用者欄位就可以了,同時我們還可以很簡單的更改表名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 { public ApplicationDbContext() : base("DefaultConnection"){} protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 預設表名是AspNetUsers,我們可以把它改成任意我們想要的 modelBuilder.Entity() .ToTable("Users"); modelBuilder.Entity() .ToTable("Users"); } } |
接下來,你就可以run一下你的網站,來體驗一把ASP.NET Identity了,別忘了先把web.config裡面的連線字串改一下,方便我們自己去檢視資料庫,只要設定一下資料庫就可以了,建立工作就交給EF吧。
我們可以在AccountController中找到所有的相關程式碼。
初始化UserManager物件
1 2 3 4 5 6 7 8 9 |
public AccountController() : this(new UserManager(new UserStore(new ApplicationDbContext()))) { } public AccountController(UserManager userManager) { UserManager = userManager; } public UserManager UserManager { get; private set; } |
登入核心程式碼
1 2 3 4 5 6 |
var user = await UserManager.FindAsync(model.UserName, model.Password); if (user != null) { await SignInAsync(user, model.RememberMe); return RedirectToLocal(returnUrl); } |
註冊核心程式碼
1 2 |
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的初始程式碼:
1 |
new UserManager(new UserStore(new ApplicationDbContext())); |
雖然所說有的方法通過UserManager來呼叫,但是最後實現的還是UserStore,並且如果我們找到UserManager的定義,會發現實際上它所接收的正是在Microsoft.AspNet.Identity.Core中定義的IUserStore介面。
1 2 3 4 |
public UserManager(IUserStore 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介面
- 作為演示,我們的使用者類就儘量簡單,只有id,使用者名稱,和密碼三個屬性
- 我們的UserStore,也只重寫了Get和Create幾個基本的方法,沒有重寫Update。
UserIdentity.cs 程式碼
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 |
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的核心程式碼
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 |
// 建立使用者 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 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核心程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 初始化 UserManager public AccountController() : this(new UserManager(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