
“跌倒了”指的是這一篇博文:愛與恨的抉擇:ASP.NET 5+EntityFramework 7
如果想了解 ASP.NET Identity 的“歷史”及“原理”,強烈建議讀一下這篇博文:MVC5 - ASP.NET Identity登錄原理 - Claims-based認證和OWIN,如果你有時間,也可以讀下 Jesse Liu 的 Membership 三部曲:
- Membership三步曲之入門篇 - Membership 基礎示例
- Membership三步曲之進階篇 - 深入剖析Provider Model
- Membership三步曲之高級篇 - 從Membership到 ASP.NET Identity
其實說來慚愧,我自己對 ASP.NET Identity 的理解及運用,僅限在使用 AuthorizeAttribute、FormsAuthentication.SetAuthCookie 等一些操作,背后的原理及其發展歷程并不是很了解,所以我當時在 ASP.NET 5 中進行身份驗證操作,才會讓自己有種“無助”的感覺,周末的時候,閱讀了 Jesse Liu 的這幾篇博文,然后又找了一些相關資料,自己似乎懂得了一些,但好像又沒有完全理解,既然說不出來,那就用“筆”記下來。
ASP.NET Identity GitHub 地址:https://github.com/aspnet/Identity
ASP.NET 5 中,關于身份驗證的變化其實不大,還是 MVC5 的那一套,只不過配置有的變化罷了,使用 VS2015 創建 MVC 項目的時候,點擊“Change Authentication”會出現下面四個選項:

如果創建的是 ASP.NET 5 項目,Authentication 默認是不可更改:

使用 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。
不知者無罪,知罪卻不贖罪,那就是有罪!!!
文章列表
請先 登入 以發表留言。