“跌倒了”指的是这一篇博文:爱与恨的抉择: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();

    // Add Identity services to the services container.
    services.AddDefaultIdentity(Configuration);
    services.AddIdentityEntityFramework(Configuration);
    services.AddIdentity(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(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 userManager, SignInManager signInManager)
    {
        UserManager = userManager;
        SignInManager = signInManager;
    }

    public UserManager UserManager { get; private set; }
    public SignInManager SignInManager { get; private set; }
}

查看整个的应用程序的代码,发现我们并没有注册 UserManager、SignInManager 类型的依赖注入,那是怎么注入的呢?其实注入的类型不是 UserManager 和 SignInManager,而是 IdentityUser,在 ConfigureServices 中我们添加过这样的代码:services.AddDefaultIdentity(Configuration);,这是最重要的,之后所有身份验证操作所用到的基类型都是从这里来的,在 IdentityServiceCollectionExtensions 中的 AddIdentity 操作中,我们发现了下面这样的代码:

services.TryAdd(describe.Scoped, UserManager>());
services.TryAdd(describe.Scoped, SignInManager>());
services.TryAdd(describe.Scoped, RoleManager>());

Scoped 所在程序集:Microsoft.Framework.DependencyInjection,DependencyInjection 为 ASP.NET 5 自带的依赖注入,如果你仔细查看其相关类型的源码,发现都是通过这个东西进行 IoC 管理的,下面我们看一个 Login 操作:

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task 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 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 Store,它就像一个仓库,为用户验证提供查询及存储服务,除了 IUserStore,在 UserManager 中,你还会发现有很多的“Store”,比如 IUserLoginStore、IUserRoleStore、IUserClaimStore 等等,但都是继承于 IUserStore,在 ConfigureServices 进行配置服务的时候,services.AddIdentity 还有一个 AddEntityFrameworkStores 方法,范型类型为 TContext,上面所有的 Store 上下文都是从它继承来的,再查看 AddEntityFrameworkStores 的实现:

public static IdentityBuilder AddEntityFrameworkStores(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 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 identities);
public virtual void SignIn(ClaimsIdentity identity);
public abstract void SignIn(AuthenticationProperties properties, IEnumerable 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(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

不知者无罪,知罪却不赎罪,那就是有罪!!!