「跌倒了」指的是這一篇博文:愛與恨的抉擇:ASP.NET 5+EntityFramework 7html
若是想了解 ASP.NET Identity 的「歷史」及「原理」,強烈建議讀一下這篇博文:MVC5 - ASP.NET Identity登陸原理 - Claims-based認證和OWIN,若是你有時間,也能夠讀下 Jesse Liu 的 Membership 三部曲:git
其實說來慚愧,我本身對 ASP.NET Identity 的理解及運用,僅限在使用 AuthorizeAttribute、FormsAuthentication.SetAuthCookie 等一些操做,背後的原理及其發展歷程並非很瞭解,因此我當時在 ASP.NET 5 中進行身份驗證操做,纔會讓本身有種「無助」的感受,週末的時候,閱讀了 Jesse Liu 的這幾篇博文,而後又找了一些相關資料,本身彷佛懂得了一些,但好像又沒有徹底理解,既然說不出來,那就用「筆」記下來。cookie
ASP.NET Identity GitHub 地址:https://github.com/aspnet/Identityapp
ASP.NET 5 中,關於身份驗證的變化其實不大,仍是 MVC5 的那一套,只不過配置有的變化罷了,使用 VS2015 建立 MVC 項目的時候,點擊「Change Authentication」會出現下面四個選項:async
若是建立的是 ASP.NET 5 項目,Authentication 默認是不可更改:ide
使用 VS2015 分別建立 MVC5 及 ASP.NET 5 的示例項目,你會發現 MVC5 中關於身份驗證的代碼及配置很是複雜,而在 ASP.NET 5 中則相對來講簡化下,首先,在 Startup.cs 文件中的 ConfigureServices 方法中,有以下配置:函數
public void ConfigureServices(IServiceCollection services) { // Add EF services to the services container. services.AddEntityFramework(Configuration) .AddSqlServer() .AddDbContext<ApplicationDbContext>(); // Add Identity services to the services container. services.AddDefaultIdentity<ApplicationDbContext, ApplicationUser, IdentityRole>(Configuration); services.AddIdentityEntityFramework<ApplicationDbContext, ApplicationUser, IdentityRole>(Configuration); services.AddIdentity<ApplicationUser, IdentityRole>(Configuration); // Add MVC services to the services container. services.AddMvc(); }
上面代碼中,AddDefaultIdentity 和 AddIdentityEntityFramework 實際上是一個意思(「捆綁銷售」),所在程序集:Microsoft.AspNet.Identity.EntityFramework,AddEntityFramework 和 AddIdentityEntityFramework 使用的是同一個 DbContext,固然也能夠進行對身份驗證上下文進行分開管理,好比咱們有可能多個應用程序共享一個身份驗證的上下文。ConfigureServices 方法的解釋爲:This method gets called by the runtime,表示這個方法在應用程序運行的時候註冊使用的服務,有點相似於組件化的應用,好比 ASP.NET 5 只是一個基礎 Web 站點,你能夠在這個應用中添加你想要的組件或模塊,好比你想使用 WebAPI,你只須要在 project.json 中添加 Microsoft.AspNet.Mvc.WebApiCompatShim 程序包,而後在 ConfigureServices 方法中進行服務註冊就好了:services.AddWebApiConventions();。
AddDefaultIdentity 註冊的三個基礎類型:
IdentityDbContext< IdentityUser >:ApplicationDbContext 繼承實現。
IdentityUser:ApplicationUser 繼承實現。
IdentityRole
註冊完成以後,就是配置使用了,在 Startup.cs 的 Configure 方法中進行配置使用:app.UseIdentity();,表示應用程序啓用身份驗證,若是把這段代碼註釋掉的話,你會發現整個應用程序的身份驗證就失效了,Configure 方法解釋是:Configure is called after ConfigureServices is called,在上面 AddDefaultIdentity 註冊中,其實包含了不少內容,關於身份驗證基本上就這三個類型,ASP.NET Identity 直接的操做經過註冊的這三個類型進行以來注入,好比後面會遇到的 UserManager 和 SignInManager,但查看這部分的源代碼,在 Microsoft.AspNet.Identity.EntityFramework 中並無加入進來。
下面咱們來根據 ASP.NET Identity 的源碼,來看一個身份驗證的流程,ASP.NET 5 中的身份驗證和以前同樣,只須要在須要驗證的 Action 上面添加 Authorize 就好了,在上面 Startup.cs 中的身份驗證配置很簡單,啓用的話只須要 app.UseIdentity(); 就能夠了,而在以前的 MVC 程序的 Web.config 中須要配置一大堆東西,在 IdentityServiceCollectionExtensions 源碼中,包含了一大堆默認配置,好比 ApplicationCookieAuthenticationType 註冊:
services.Configure<CookieAuthenticationOptions>(options => { options.AuthenticationType = IdentityOptions.ApplicationCookieAuthenticationType; options.LoginPath = new PathString("/Account/Login"); options.Notifications = new CookieAuthenticationNotifications { OnValidateIdentity = SecurityStampValidator.ValidateIdentityAsync }; }, IdentityOptions.ApplicationCookieAuthenticationType);
咱們也能夠在 Configure 中進行自定義配置,配置方法:app.UseCookieAuthentication。當訪問 Action 的身份驗證失效後,跳轉到「/Account/Login」進行登陸,查看 AccountController 中的示例代碼,你會發現有下面的東西:
public class AccountController : Controller { public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager) { UserManager = userManager; SignInManager = signInManager; } public UserManager<ApplicationUser> UserManager { get; private set; } public SignInManager<ApplicationUser> SignInManager { get; private set; } }
查看整個的應用程序的代碼,發現咱們並無註冊 UserManager、SignInManager 類型的依賴注入,那是怎麼注入的呢?其實注入的類型不是 UserManager 和 SignInManager,而是 IdentityUser,在 ConfigureServices 中咱們添加過這樣的代碼:services.AddDefaultIdentity<ApplicationDbContext, ApplicationUser, IdentityRole>(Configuration);
,這是最重要的,以後全部身份驗證操做所用到的基類型都是從這裏來的,在 IdentityServiceCollectionExtensions 中的 AddIdentity 操做中,咱們發現了下面這樣的代碼:
services.TryAdd(describe.Scoped<UserManager<TUser>, UserManager<TUser>>()); services.TryAdd(describe.Scoped<SignInManager<TUser>, SignInManager<TUser>>()); services.TryAdd(describe.Scoped<RoleManager<TRole>, RoleManager<TRole>>());
Scoped 所在程序集:Microsoft.Framework.DependencyInjection,DependencyInjection 爲 ASP.NET 5 自帶的依賴注入,若是你仔細查看其相關類型的源碼,發現都是經過這個東西進行 IoC 管理的,下面咱們看一個 Login 操做:
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) { if (ModelState.IsValid) { var signInStatus = await SignInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: false); switch (signInStatus) { case SignInStatus.Success: return RedirectToLocal(returnUrl); case SignInStatus.Failure: default: ModelState.AddModelError("", "Invalid username or password."); return View(model); } } // If we got this far, something failed, redisplay form return View(model); }
最主要的操做是,經過 ASP.NET Identity 的 SignInManager.PasswordSignInAsync 操做,進行驗證身份密碼,返回 SignInStatus 類型的驗證結果:
public enum SignInStatus { Success = 0, LockedOut = 1, RequiresVerification = 2, Failure = 3 }
咱們來看一下 SignInManager.PasswordSignInAsync 中究竟幹了什麼事:
public virtual async Task<SignInResult> PasswordSignInAsync(TUser user, string password, bool isPersistent, bool shouldLockout, CancellationToken cancellationToken = default(CancellationToken)) { if (user == null) { throw new ArgumentNullException(nameof(user)); } var error = await PreSignInCheck(user, cancellationToken); if (error != null) { return error; } if (await IsLockedOut(user, cancellationToken)) { return SignInResult.LockedOut; } if (await UserManager.CheckPasswordAsync(user, password, cancellationToken)) { await ResetLockout(user, cancellationToken); return await SignInOrTwoFactorAsync(user, isPersistent, cancellationToken); } if (UserManager.SupportsUserLockout && shouldLockout) { // If lockout is requested, increment access failed count which might lock out the user await UserManager.AccessFailedAsync(user, cancellationToken); if (await UserManager.IsLockedOutAsync(user, cancellationToken)) { return SignInResult.LockedOut; } } return SignInResult.Failed; }
PasswordSignInAsync 還有一個重寫方法,是獲取用戶信息的:UserManager.FindByNameAsync(userName, cancellationToken);
,接着查看 FindByNameAsync 的定義,會找到這段代碼:Store.FindByNameAsync(userName, cancellationToken)
,Store 是什麼?類型定義爲:IUserStore<TUser> Store
,它就像一個倉庫,爲用戶驗證提供查詢及存儲服務,除了 IUserStore,在 UserManager 中,你還會發現有不少的「Store」,好比 IUserLoginStore、IUserRoleStore、IUserClaimStore 等等,但都是繼承於 IUserStore,在 ConfigureServices 進行配置服務的時候,services.AddIdentity 還有一個 AddEntityFrameworkStores 方法,範型類型爲 TContext,上面全部的 Store 上下文都是從它繼承來的,再查看 AddEntityFrameworkStores 的實現:
public static IdentityBuilder AddEntityFrameworkStores<TContext>(this IdentityBuilder builder) where TContext : DbContext { builder.Services.Add(IdentityEntityFrameworkServices.GetDefaultServices(builder.UserType, builder.RoleType, typeof(TContext))); return builder; }
builder.Services.Add 所起到的做用就是往 IoC 中注入類型,這樣全部用到此類型的引用,均可以經過構造函數注入方式獲取其實現,再查看 GetDefaultServices 的具體實現,由於看不懂代碼,就不貼出來了,其實裏面操做的就三個類型:TUser、TRole 和 TContext,這也是 ASP.NET Identity 操做的三個基本類型,在 Identity 操做中,基本上是兩大操做類,一個是 SignInManager,另外一個就是 UserManager,其實查看
SignInManager 的具體實現代碼,你會發現,關於用戶的獲取及存儲,都是經過 UserManager 進行操做的,而 UserManager 又是經過 IUserStore 的具體實現類進行操做的,SignInManager 只不過是一個用戶驗證的操做類,好比咱們一開始說到的 SignInManager.PasswordSignInAsync,上面已經貼出代碼了,你會看到基本上都是 UserManager.什麼,好比 UserManager.CheckPasswordAsync、UserManager.SupportsUserLockout、UserManager.AccessFailedAsync 等等,在 PasswordSignInAsync 代碼實現中,不關於用戶操做的,最核心的就是這段代碼:SignInOrTwoFactorAsync(user, isPersistent, cancellationToken);
,查看其具體實現:
private async Task<SignInResult> SignInOrTwoFactorAsync(TUser user, bool isPersistent, CancellationToken cancellationToken, string loginProvider = null) { if (UserManager.SupportsUserTwoFactor && await UserManager.GetTwoFactorEnabledAsync(user, cancellationToken) && (await UserManager.GetValidTwoFactorProvidersAsync(user, cancellationToken)).Count > 0) { if (!await IsTwoFactorClientRememberedAsync(user, cancellationToken)) { // Store the userId for use after two factor check var userId = await UserManager.GetUserIdAsync(user, cancellationToken); Context.Response.SignIn(StoreTwoFactorInfo(userId, loginProvider)); return SignInResult.TwoFactorRequired; } } // Cleanup external cookie if (loginProvider != null) { Context.Response.SignOut(IdentityOptions.ExternalCookieAuthenticationType); } await SignInAsync(user, isPersistent, loginProvider, cancellationToken); return SignInResult.Success; }
再次拋開一大堆的 UserManager 操做,找到最核心的:Context.Response.SignIn(StoreTwoFactorInfo(userId, loginProvider))
,StoreTwoFactorInfo 方法返回類型爲 ClaimsIdentity,在返回以前,根據 userId 建立 Claim 對象,並添加到 ClaimsIdentity 集合中,接下來的操做就是:Context.Response.SignIn
,將用戶身份信息輸入到當前上下文,接着查看 HttpResponse 抽象類關於 SignIn 的定義:
public virtual void SignIn(IEnumerable<ClaimsIdentity> identities); public virtual void SignIn(ClaimsIdentity identity); public abstract void SignIn(AuthenticationProperties properties, IEnumerable<ClaimsIdentity> identities); public virtual void SignIn(AuthenticationProperties properties, params ClaimsIdentity[] identities); public virtual void SignIn(AuthenticationProperties properties, ClaimsIdentity identity);
在之前若是使用 SignIn,其調用方式是 System.Web.Security.FormsAuthentication.SetAuthCookie("userName", false);
,採用的是 Forms 認證,可是在 ASP.NET 5 中,已經訪問不到 SetAuthCookie 了,原來的 SetAuthCookie 實現方式不知道是怎樣的,若是在 ASP.NET 5 中實現 SetAuthCookie 相似的效果,咱們該怎麼作呢?只須要在 Startup.cs 的 Configure 方法中進行下面配置:
//app.UseIdentity(); app.UseCookieAuthentication((cookieOptions) => { cookieOptions.AuthenticationType = IdentityOptions.ApplicationCookieAuthenticationType; cookieOptions.AuthenticationMode = AuthenticationMode.Active; cookieOptions.CookieHttpOnly = true; cookieOptions.CookieName = ".CookieName"; cookieOptions.LoginPath = new PathString("/Account/Login"); //cookieOptions.CookieDomain = ".mysite.com"; }, "AccountAuthorize");
其實咱們下面進行自定義的配置和上面註釋的 UseIdentity 是同樣的效果,只不過有些操做是在 Microsoft.AspNet.Identity.IdentityServiceCollectionExtensions 中默認完成的,注意上面配置中,咱們將以前的 services.AddDefaultIdentity<ApplicationDbContext, ApplicationUser, IdentityRole>(Configuration);
代碼給註釋了,再來看下 Account 中的 Login 代碼:
[AllowAnonymous] public void Login(string returnUrl = null) { var userId = "xishuai"; var identity = new ClaimsIdentity(IdentityOptions.ApplicationCookieAuthenticationType); identity.AddClaim(new Claim(ClaimTypes.Name, userId)); Response.SignIn(identity); }
上面的操做其實就是以前的 SignInManager.PasswordSignInAsync
同樣,只不過是一個簡化版本,另外,IdentityOptions.ApplicationCookieAuthenticationType 也沒什麼神奇的地方,就是一個類型字符串:
public static string ApplicationCookieAuthenticationType { get; set; } = typeof(IdentityOptions).Namespace + ".Application"; public static string ExternalCookieAuthenticationType { get; set; } = typeof(IdentityOptions).Namespace + ".External"; public static string TwoFactorUserIdCookieAuthenticationType { get; set; } = typeof(IdentityOptions).Namespace + ".TwoFactorUserId"; public static string TwoFactorRememberMeCookieAuthenticationType { get; set; } = typeof(IdentityOptions).Namespace + ".TwoFactorRemeberMe";
Login 登陸驗證效果:
總結:上面也說了很多內容,說真的,其實我也不知道本身說了什麼,有幾點感觸鬚要總結下,在多個應用程序共享身份驗證的時候(CookieDomain),不論是使用 FormsAuthentication,仍是使用 SignInManager.SignInAsync,又或者使用 UserStore 進行用戶管理,但用戶進行驗證的程序只有一個,這個按照本身的想法,想怎麼實現就怎麼實現,其餘的應用程序都只不過是判斷用戶是否經過驗證請求、及獲取用戶標識的,就這兩個操做,用戶的驗證無論上面的何種實現,咱們均可以經過 User.Identity 獲取用戶驗證的信息,類型爲 IIdentity。
不知者無罪,知罪卻不贖罪,那就是有罪!!!