從Membership 到 .NET4.5 之 ASP.NET Identity
我們前面已經(jīng)討論過了如何在一個網(wǎng)站中集成最基本的Membership功能,然后深入學(xué)習(xí)了Membership的架構(gòu)設(shè)計(jì)。正所謂從實(shí)踐從來,到實(shí)踐從去,在我們把Membership的結(jié)構(gòu)吃透之后,我們要完善它,改造它,這樣我們才能真正學(xué)以致用。今天我們將以用戶信息為主線,從SqlMembershipProvider出發(fā),到ASP.NET Simple Membership最后再到MV5中引入的ASP.NET Identity,來看看微軟是如何一步一步的改造這套框架的。
- Membership三步曲之入門篇 - Membership 基礎(chǔ)示例
- Membership三步曲之進(jìn)階篇 - 深入剖析Provider Model
- Membership三步曲之高級篇 - 從Membership到 ASP.NET Identity
內(nèi)容索引
- 引入 - 用戶信息是如何存在數(shù)據(jù)庫中的?
- ProfileProvider來擴(kuò)展用戶信息
- Simple Membership Provider
- ASP.NET Identity
- 小結(jié) & 示例代碼下載
引入 - 用戶信息是如何存在數(shù)據(jù)庫中的
我們前兩篇都只講到了怎么用Membership注冊,登錄等,但是我們漏掉了一個很重要并且是基本上每個用Membership的人都想問的,我的用戶信息怎么保存?我不可能只有用戶名和密碼,如果我要加其它的字段怎么辦?我們首先來看一下,SqlMembershipProvider是如何做的,畢竟這個Provider是跟著Membership框架一起誕生出來的。
ASP.NET 2.0時(shí)代,我們需要借助一個VS提供的一個工具來幫助我們生成所需要的表。打開VS 開發(fā)者命令行工具,輸入aspnet_regsql,后面簡單的連接一下數(shù)據(jù)庫就會幫我們生成以下的幾張表:

我們這里簡要關(guān)注以下幾張表的結(jié)構(gòu)就可以了。


我想上面兩張圖應(yīng)該可以說明很多問題,用戶信息的一些基本字段比如用戶名,密碼以及一些其它登錄的信息存儲在哪里,角色存儲在哪里,角色和用戶之間是如何關(guān)聯(lián)的等等,但是還有正如本節(jié)標(biāo)題所說的一樣,用戶信息字段如何擴(kuò)展呢?
ProfileProvider 來擴(kuò)展用戶信息
我們上面講到有一張表aspnet_Profile是專門用來給ProfileProvider為擴(kuò)展用戶信息的。它和MebershipProvider, RoleProvider一起組成了用戶信息,權(quán)限管理這樣一套完整的框架。下面我們就來看看如何用ProfileProvider來擴(kuò)展我們想要的用戶信息。
- 我們先添加一個Model繼承ProfileBase來為我們新的用戶對象建模
- 在web.config配置ProfileProvider
- 在MVC站點(diǎn)中實(shí)現(xiàn)對我們的用戶信息的管理
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結(jié)點(diǎn)我們設(shè)置了我們上一步建立的那個對象,這樣我們就可以在代碼將MVC里面的Profile對象轉(zhuǎn)換成我們要的這些類型。
從Profile對象中獲取當(dāng)前登錄用戶的信息
public ActionResult Manage()
{
var profile = Profile as UserProfile;
var model = new UserProfileViewModel
{
FirstName = profile.FirstName,
LastName = profile.LastName
};
return View(model);
}
保存當(dāng)前用戶的信息
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);
}
怎么樣?是不是不復(fù)雜?加上我們前面學(xué)到的MembershipProvider,RoleProvider那么我們很輕松就可以將這一系列登錄、授權(quán)、認(rèn)證以及用戶模塊相關(guān)的功能完成了。如果要使用ProfileProvider的話,最好是在最開始的設(shè)計(jì)階段就使用,因?yàn)橐氚裀rofileProvider直接集成到現(xiàn)有的老系統(tǒng)中,那是一件很難的事情,我們看一下Profile表的結(jié)構(gòu)就知道了。

Profile要做到通用,那么這張表就要求能夠存儲任意類型的數(shù)據(jù),所以微軟就采用一種這樣的設(shè)計(jì),把所有的字段以string的格式放到了一列中,然后再解析出來。別的先不說,首先這種設(shè)計(jì)對于大型系統(tǒng)來說,肯定會有一個性能的瓶頸,并且如果我們想要把ProfileProvider集成到老的系統(tǒng)中,那會是一件很難的事情。那么微軟后面做了哪些改進(jìn)呢?
Simple Membership Provider
假想一下,你使用了SQL Membership Provider,你想抱怨哪些問題呢?
- 最先抱怨的肯定是沒有辦法自定義用戶信息,必須要通過ProfileProvider,那玩意兒真心不好用!
- 其實(shí)與現(xiàn)有或其它系統(tǒng)集成簡直是太麻煩了?。?/li>
- 數(shù)據(jù)表都被你定義好了,但是很抱歉,那都不是我想要的啊?。?!
- 等等。。。
好吧,這些問題確實(shí)是導(dǎo)致Membership一直不溫不火的原因之一。 所有這就是為什么后來,我們有了Simple Mebership Provider,借助于它:
- 我們不必再依懶于Profile Provider去擴(kuò)展用戶信息。
- 可以完全讓Membership 根據(jù)我們自己定義的表結(jié)構(gòu)來運(yùn)行。
- 與Entity Framework集成,好吧(微軟這是捆綁銷售么? 慣用伎倆)
- 另外,在VS2012或2013中創(chuàng)建一個MVC4.0的Internet程序,就會為你自動添加所有代碼!
最后一招夠狠,我們來試一下。在VS2012中創(chuàng)建一個4.0 的MVC站點(diǎn),就可以在Controllers和Models中發(fā)現(xiàn)相關(guān)代碼,在AccountController中已經(jīng)有了登錄注冊相關(guān)的代碼。

在AccountModel中,我們可以找到一個UserProfile的類就是一個Entity Framework 的實(shí)體類。
[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來整個用戶實(shí)體類做登錄模塊有啥區(qū)別? 我也懷疑區(qū)別就是可以在創(chuàng)建membership用戶記錄的時(shí)候,可以一起把我們的額外信息帶進(jìn)去,其余的還真沒有發(fā)現(xiàn)什么區(qū)別。SimpleMembershipProvider所有的操作都是通過WebSecurity這個類來完成的,這個類所完成的功能與Membershipo類是一樣的,主要是對Provider的功能進(jìn)行一個封裝,而這個類是包含在WebMatrix.WebData.dll中的。打開網(wǎng)站的引用目錄發(fā)現(xiàn)引用了WebMatrix.Data和WebMatrix.WebData這兩個dll。這兩個dll主要是給web page用的, 而SimpleMembershipProvider的相關(guān)代碼就包含在這兩個dll當(dāng)中。


里面怎么實(shí)現(xiàn)的我想就不用詳述了,無非就是繼承MembershipProvider然后覆蓋其中的一些方法而已。我們Membership系列第二篇已經(jīng)詳述過了,有興趣的同學(xué)請移步。在后來微軟還推出來Universal Providers,用來幫助Membership轉(zhuǎn)移到Windows Azure的以及對SQL Compact的支持。
ASP.NET Identity
基礎(chǔ)示例
ASP.NET Identity是在.NET Framework4.5中引入的,從Membership發(fā)布以來,我想微軟已經(jīng)從開發(fā)者以及企業(yè)客戶那里面得到了足夠的反饋信息來幫助他們打造這樣一套新的框架。他所擁有的特點(diǎn)大多也是前面所不能滿足的,至少我們看到的是進(jìn)步,不是么?
- 一套ASP.NET Identity,可以用于ASP.NET下的web form, MVC, web pages, web API等
- 和Simple Membership Provider,可以靈活訂制用戶信息,同樣采用EF Code First來完成數(shù)據(jù)操作
- 完全自定義數(shù)據(jù)結(jié)構(gòu)
- 單元測試的支持
- 與Role Provider集成
- 支持面向Clamis的認(rèn)證
- 支持社交賬號的登錄
- OWIN 集成
- 通過NuGet發(fā)布來實(shí)現(xiàn)快速迭代
瞟一眼好處還真不少,但是至少對于開發(fā)者來說,好用,能滿足需求,靈活才是王道,那我們下面就來看看如何使用ASP.NET Identity來完成我們的用戶授權(quán)和認(rèn)證模塊。其實(shí)我們已經(jīng)不用寫任何示例代碼,因?yàn)槲覀冎灰褂肰S創(chuàng)建一個.NET Framework 4.5 的 MVC站點(diǎn),所有的代碼都已經(jīng)包括了。
默認(rèn)創(chuàng)建的IdentityModels.cs
public class ApplicationUser : IdentityUser
{ }
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
: base("DefaultConnection")
{
}
}
我們需要在ApplicaitonUser實(shí)體中添加我們的用戶字段就可以了,同時(shí)我們還可以很簡單的更改表名。
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);
// 默認(rèn)表名是AspNetUsers,我們可以把它改成任意我們想要的
modelBuilder.Entity<IdentityUser>()
.ToTable("Users");
modelBuilder.Entity<ApplicationUser>()
.ToTable("Users");
}
}
接下來,你就可以run一下你的網(wǎng)站,來體驗(yàn)一把ASP.NET Identity了,別忘了先把web.config里面的連接字符串改一下,方便我們自己去查看數(shù)據(jù)庫,只要設(shè)置一下數(shù)據(jù)庫就可以了,創(chuàng)建工作就交給EF吧。

我們可以在AccountController中找到所有的相關(guān)代碼。
初始化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);
框架設(shè)計(jì)
我們上面是直接利用VS幫助我們創(chuàng)建好了一些初始代碼,我們也可以創(chuàng)建一個空白的站點(diǎn),然后再把ASP.NET Identity引用進(jìn)來。所需要的類庫可以直接從Nuget上下載就可以了。
主要包括ASP.NET Identity 的EF 部分的實(shí)現(xiàn),有了EF的幫助我們就可以完全自定義數(shù)據(jù)結(jié)構(gòu),當(dāng)然我們也只需要定義一個實(shí)體類就可以了?! ?/p>
名字就已經(jīng)告訴大家了,這是ASP.NET Identity的核心了,所以主要的功能在這里面。上面那個包是ASP.NET Identity EF的實(shí)現(xiàn),那么我們可以在這個核心包的基礎(chǔ)上擴(kuò)展出基于No SQL, Azure Storage 的 ASP.NET Identity實(shí)現(xiàn)。
ASP.NET Identity對OWIN 認(rèn)證的支持。

最上面兩個就是我們自己創(chuàng)建的代碼,分別繼承自己Microsoft.AspNet.Identity.EntityFramework的IdentityUser和IdentityDbContext。但是最后別忘了,我們與用戶相關(guān)的操作實(shí)際上是通過Microsoft.AspNet.Identity.Core的 UserManager類來完成的。通過這樣一種設(shè)計(jì),可以把具體定義和實(shí)現(xiàn)交給上層,但是最后的核心卻完全由自己掌控,實(shí)現(xiàn)松耦合,高內(nèi)聚(一不小心我竟然說出了這么專業(yè)的解釋,小心臟砰砰跳呀?。?/p>
框架實(shí)現(xiàn)剖析
上面只是一張粗略的類圖,下面我們就來看一下這些類之間是如何關(guān)聯(lián)起來協(xié)作的。我們通過上面基礎(chǔ)示例的代碼可以發(fā)現(xiàn),用用戶相關(guān)的功能是通過調(diào)用UserManager的方法來完成的。 我們可以在AccountController中找到UserManager的初始代碼:
new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));
雖然所說有的方法通過UserManager來調(diào)用,但是最后實(shí)現(xiàn)的還是UserStore,并且如果我們找到UserManager的定義,會發(fā)現(xiàn)實(shí)際上它所接收的正是在Microsoft.AspNet.Identity.Core中定義的IUserStore接口。
public UserManager(IUserStore<TUser> store)
{
this.Store = store;
}
我們現(xiàn)在使用的是ASP.NET Identity EF的實(shí)現(xiàn),所以在UserStore中,直接調(diào)用傳進(jìn)來的DbContext的Save操作就可以了。

有沒有發(fā)現(xiàn)這張圖和我們第二篇中講的Provider模式有那么點(diǎn)點(diǎn)的神似? 在Membership中,我們所有的操作通過調(diào)用Membership來過多成,但是Membership本身只是一個包裝類,內(nèi)部的操作實(shí)際上是通過Provider的實(shí)際類來完成的,這就是策略模式的典型案例。只不過Membership的Provider通過web.config配置完成,而UserManager通過構(gòu)造函數(shù)注入完成。
擴(kuò)展ASP.NET Identity - 將用戶信息寫入文件
為了熟悉AspNet.Identity的結(jié)構(gòu),我們來擴(kuò)展實(shí)現(xiàn)一個將用戶信息寫入文件的組件,然后實(shí)現(xiàn)登錄注冊功能,我們就給它命名AspNet.Identity.File吧。
- 創(chuàng)建一個自己的用戶類(UserIdentity)實(shí)現(xiàn)Microsoft.AspNet.Identity.IUser接口
- 創(chuàng)建一個自己的UserStore類實(shí)現(xiàn)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的核心代碼
// 創(chuàng)建用戶
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());
}
}
// 根據(jù)用戶名找用戶
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)
{
// 創(chuàng)建用戶成功
}
保存到txt中的用戶信息

小結(jié)
Membership系列這三篇,從入門到精通到這里就算是結(jié)束了,不知道能不能算是園滿。因?yàn)檫@三篇的關(guān)注度都不是很高,可能沒有從多少人在乎這個玩意。不過還是要感謝@好玩一人的催促,讓我堅(jiān)持把這三篇寫完了。可能Membership不是.NET里面非常成功的一部份,但是這并不能說它不好,而是因?yàn)橄襁@種需求的東西如果要做成類庫本身就是一項(xiàng)比較困難的事情,因?yàn)閹缀鹾苌儆幸荒R粯拥男枨蟆?/p>
但是我們更應(yīng)該關(guān)注的是微軟是如何面對復(fù)雜多變的需求來設(shè)計(jì)框架的,如何從一大堆的零散需求中找出最核心的部份, 他們?nèi)绾谓怦睿绾翁岣呖蓴U(kuò)展性和維護(hù)性的。從Membersihp引入.NET的時(shí)候給我們帶來了Provider,于是我們會發(fā)現(xiàn).NET2.0開始就出現(xiàn)了各種Provider,web.config里面各種配置。而最新的ASP.NET Identity已經(jīng)不再用那樣的Provider模式了,但是思想?yún)s大致相同,只不過換成了用范型來實(shí)現(xiàn),用構(gòu)造函數(shù)注入,這也是從MVC以來微軟框架的一些特色。而我們,在追求微軟技術(shù)的同時(shí),更應(yīng)該理解其內(nèi)在的一些思想和本質(zhì),這樣才不致于被淹沒在無盡的新技術(shù)中,因?yàn)楹芏嗥鋵?shí)只是換湯不換藥,或者我們可以用積極的話來說,微軟在不斷的提高開發(fā)人員的效率,并且讓你寫代碼的時(shí)候有更好的心情。 請相信我,理解了本質(zhì),再去學(xué)習(xí)新技術(shù),能讓你效率翻倍。
最后,還是謝謝大家一直的關(guān)注和陪伴。
下面的demo的鏈接下載,包括一個ProfileProvider的例子,和后面將用戶信息寫入txt文件的例子。
AspNet.Identity.File: http://pan.baidu.com/s/1dD5SZ1v
ProfileProvider Demo: http://pan.baidu.com/s/1bnnakZt

浙公網(wǎng)安備 33010602011771號