在之前的文章中,我為大家介紹了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
-
<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等,當然我們也可以在其派生類中添加額外的信息,代碼如下:
-
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),代碼如下所示:
-
public class AppIdentityDbContext : IdentityDbContext<AppUser>
-
{
-
{
-
}
-
-
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中操作用戶的方法。代碼如下所示:
-
/// <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中,如下所示:
-
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獲取對象:
-
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集合,如下所示:
-
public ActionResult Index()
-
{
-
return View(UserManager.Users);
-
}
創建User對象
通過UserManager的CreateAsync方法,可以快速的創建User對象,如下代碼創建了User ViewModel:
-
public class UserViewModel
-
{
-
[Required]
-
public string Name { get; set; }
-
[Required]
-
public string Email { get; set; }
-
[Required]
-
public string Password { get; set; }
-
}
使用UserManager對象的CreateAsync方法將AppUser對象將它持久化到數據庫:
-
[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 處,如下所示:
-
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方法即可,如下代碼所示:
-
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 屬性上,如下代碼所示:
-
//自定義的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 方法,如下所示:
-
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 對象上:
-
//自定義的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來刪除它,如下所示:
-
[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策略,這樣確保沒有臟數據,如下所示:
-
[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之旅,探索身份驗證和授權的使用,謝謝 。