ASP.NET 5 Identity
“跌倒了”指的是这一篇博文:爱与恨的抉择: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。
不知者无罪,知罪却不赎罪,那就是有罪!!!