• 跌倒了,再爬起来:ASP.NET 5 Identity


    “跌倒了”指的是这一篇博文:爱与恨的抉择:ASP.NET 5+EntityFramework 7

    如果想了解 ASP.NET Identity 的“历史”及“原理”,强烈建议读一下这篇博文:MVC5 - ASP.NET Identity登录原理 - Claims-based认证和OWIN,如果你有时间,也可以读下 Jesse Liu 的 Membership 三部曲:

    其实说来惭愧,我自己对 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

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

  • 相关阅读:
    AWTK-MVVM 在 STM32H743 上的移植笔记
    windows 中文 unicode 编码显示
    SpringBoot项目jar包运行
    Activiti中的互斥网关、并行网关、兼容网关、事件网关
    【LeetCode】739.每日温度(5种方法,详细图解)
    【LeetCode】20.有效的括号(使用栈,动图详解)
    你知道权限管理的RBAC模型吗?
    关闭Win10自动更新
    iOS 中如何判断当前是2G/3G/4G/5G/WiFi
    GCD API 记录 (三)
  • 原文地址:https://www.cnblogs.com/xishuai/p/asp-net-5-identity.html
Copyright © 2020-2023  润新知