• ASP.NET Core 身份验证(一)


    前言

    这篇文章我想带领大家了解一下 ASP.NET Core 中如何进行的身份验证,在开始之前强烈建议还没看过我写的 Identity 系列文章的同学先看一下。

    Identity 入门系列文章:

    名词解释

    做 Web 开发的都知道 HTTP 协议是无状态的,那么服务端如果想知道此次请求的用户是哪个登录的用户,那么就需要有一种标识每次都被传递到服务端,那么这个标识就是我们都知道的 Cookie(这里我们先不考虑header中携带标识的情况),服务端根据 Cookie 中携带的信息进行识别的一个过程就是身份验证,所有基于 WEB 的服务端都是如此,无关乎语言和框架。

    在整个身份验证的过程中,又分为两个部分即认证和授权,很多同学区分不出来这两个东西,因为这两个单词看起来有点像,导致经常认错,这里我教大家一个小方法,就是记住他们的发音,使用某种方法让发音和汉字对应起来,这样就记住了。

    Authentication [ɔ:,θenti'keiʃən] 认证

    Authorization [,ɔ:θərai'zeiʃən, -ri'z-] 授权

    分享一下我的方法,认证的拼音是(renzheng),其中 zheng 包含 en ,同样的 Authentication 也包含 en,这样我就记住了这个单词是认证,那么另外一个就是授权了。

    认证:确定用户身份的一个过程。 注意是一个过程。

    授权:确认用户可以做哪些事情,即权限。

    基于 Claims 的身份

    在 ASP.NET Core 中主要是使用的基于 Claims 的身份验证,也就是说将用户的属性都抽象成证件单元来表示了,通过证件单元来表示一张身份证。

    我们先来回顾一下如何制造一张身份证:

    //证件单元
    var claims = new List<Claim>()
    {
        new Claim(ClaimTypes.Name,"奥巴马"),
        new Claim(ClaimTypes.NameIdentifier,"身份证号")
    };
    
    //使用证件单元创建一张身份证
    var identity = new ClaimsIdentity(claims, "AuthenticationTypeXXX");
    
    

    注意,在 new ClaimsIdentity 的时候第二个参数是 AuthenticationType,我在前面文章中讲过这个是 载体类型,也就是实体形式的身份证,对吧?

    那么,在使用程序创建一个身份的时候,需要就指定这个载体了,在HTTP验证中,我们将载体设置为Cookies,代码如下:

    var cookie身份证 = new ClaimsIdentity(claims, "Cookies");
    

    有了Cookie身份证,我们还需要一个携带者,看过之前文章的可能知道,我讲 ClaimsPrincipal 的时候,一张身份证就不是代表一个人了,而是不通的身份种类,比如你可以同时是一名教师,母亲,商人。如果你想证明你同时有这几种身份的时候,你可能需要出示教师证,你孩子的出生证,法人代表的营业执照证。

    所以,我们还需要制造一个人,这个人来携带各种证件,我们就携带上一步制造的 cookie身份证 吧,先携带这一个好了:

    var 人 = new ClaimsPrincipal(cookie身份证)
    

    我们来看一下完整的一个代码

    //证件单元
    var claims = new List<Claim>()
    {
        new Claim(ClaimTypes.Name,"奥巴马"),
        new Claim(ClaimTypes.NameIdentifier,"身份证号")
    };
    
    //使用证件单元创建一张cookie身份证
    var cookie身份证 = new ClaimsIdentity(claims, "Cookies");
    
    //创建一个人携带cookie身份证
    var 人 = new ClaimsPrincipal(cookie身份证)
    
    

    多重身份

    当一个人有多种身份的时候,这个时候可能有人会问,什么情况下会有多种身份呢?

    举个简单的例子,上面的 cookie身份证 算是一种身份,那么我可能还有比如接入 OAuth的时候使用的 bearer身份证,接入第三方登录时候使用过的 google身份证facebook身份证microsoft身份证 等等,这就叫多重身份

    多种身份种的每一种身份都有一个 AuthenticationType 对应一个认证方式,后面我会讲到。

    以上,我们理清楚了一个重要的逻辑关系就是:

    一个人有多种身份,每个身份都有证件单元和一个认证方式组成。

    接下来,你们可能就会认为我就开始介绍认证和授权了。 不,很多东西有时候和你想象的并不一样,比如这篇文章也是,所以接下来我要讲的东西是 IdentityModel

    IdentityModel

    IdentityModel 是一种基于 Claim 的 Identity 库,它提供了一组类用来标识用户身份,以及对这些东西的抽象。

    有些同学可能会问,不是已经有 ClaimsIdentity 来表示用户身份了吗?为啥又还有其他的表示用户身份的东西呢?

    大哥,身份认证是一整套复杂的东西,包含很多组件,协议,标准,如果很简单就学会了我还用得着写文章教你吗? 还是接着介绍吧。

    最初,IdentityModel 是属于 WIF(Windows Identity Foundation) 的一部分,WIF 是微软2004年给 .NET 平台搞的一套身份验证框架(包含Claims,Configuration,Metadata,Policy,Servicesd等等),微软想把这个东西作为 .NET 标准框架的一部分,所以它的命名空间是 System.IdentityModel, 了解这个东西的人不是很多,不过不知道也没关系,反正这玩意也已经被淘汰了。

    在 .NET Core 中, WIF 这些套件只有 System.IdentityModel.Tokens.Jwt 被保留了下来,其他全被扔掉了,为什么呢?

    原因是只有 JWT 这部分东西有用,其他的部分更多的是为以前的 Web Servics, WCF 那套分布式东西设计的,那套分布式的东西淘汰了,自然也不必要保留了。

    在没有 .NET Core 的时候,我们想实现一套标准的单点登录(SSO)系统就可以利用 System.IdentityModel 因为它已经为我们做了大量工作,并且是标准化的。在 .NET Core 中也需要一些标准的抽象东西那怎么办呢?

    微软弄了一套新的 IdentityModel 的库,命名空间为 Microsoft.IdentityModel。很多人甚至都找不到它的源码在哪里,我一开始也没找到,最后发现在 https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet 这个仓库里面。

    这个库的组成部分同样都是抽象的部分,包括相关的协议对象,票据的加解密,票据存储 等等,也就是说微软给 .NET Core 的身份验证体系又定义了一套抽象的东西,任何第三方基于身份验证的实现库或者框架都要遵循(依赖)他们。

    以上的关于 IdentityModel 的介绍和下面我要将的东西关系不是很大,之所以要在这里引入是因为我要为后续的文章做铺垫,在这里引入最合适不过。

    接下来我们继续讲解,就开始了认证部分的讲解。

    Authentication 认证

    我之前讲过奥巴马去杭州旅游的故事,有些同学反映还是看不懂,所以我决定这次配合 ASP.NET Core 中 Cookie 身份认证的过程来讲解。

    再次声明,如果你还没看过 Identity 入门一 这篇文章,我要求你先跳过去看一下,因为接下来的内容是这篇文章的延申。

    我们假设你现在已经知道了身份证,然后现在使用身份证是坐火车。

    就是奥巴马

    身份证就是 cookie身份证

    我们将开始我们的认证旅程,同时结合我们最熟悉的 HTTP 登录流程。

    奥马巴要去乘坐火车,那么现在他要过安检,在Web登录中就是对应的登录,登录要使用用户名密码,但是用户名密码是属于业务逻辑方面的验证,我们不考虑,因为假设是第三方登录就不需要输入用户名和密码了,所以你可以理解为我们假设用户名和密码都正确,现在奥马巴要过安检了。

    对应的代码为:

    //证件单元
    var claims = new List<Claim>()
    {
        new Claim(ClaimTypes.Name,"奥巴马"),
        new Claim(ClaimTypes.NameIdentifier,"身份证号")
    };
    
    //使用证件单元创建一张身份证
    var identity = new ClaimsIdentity(claims,"Cookies");
    
    //使用身份证创建一个证件当事人,也就是奥巴马
    var identityPrincipal = new ClaimsPrincipal(identity);
    
    //奥巴马开始过安检
    await HttpContext.SignInAsync("Cookies", identityPrincipal);
    
    

    现在,我们来运行程序,看看会发生什么。你先不用管 HttpContext.SignInAsync 是做什么用的,下面会说。

    新建一个ASP.NET Core 空的 MVC 程序,然后在登录的 Action 方法中粘贴以上代码,然后按 F5 运行。

    出错了,根据错误信息我们可以看出是因为我们没有注册身份验证的中间件,而且错误已经告诉了我们应该怎么做,我们尝试解决这个错误。

    Startup.cs 文件中 ConfigureServices 方法注册服务

    public void ConfigureServices(IServiceCollection services)
    {
        ...
        
        services.AddAuthentication("Cookies")
            .AddCookie("Cookies"); 
            
        ...
    } 
    

    注意,AddAuthentication 这里是指定默认的认证载体类型,AddCookie 这里是注册载体类型的处理程序。

    认证部分我会在下一篇中详细介绍,所以这里先大致了解下。

    再次 F5 运行发现已经正常了。

    我们打开浏览器的 Cookie 查看一下,可以看到多了一项 Cookie 记录

    我们可以看到这个 Cookie 的 Name 为 .AdpNetCore.Cookie,Value 为一大长串加密的字符串。

    流程讲解

    现在我来开始讲 HttpContext.SignIn

    它是一个扩展方法,最终是调用的 IAuthenticationService 接口的 SignInAsync 方法。我们来看下接口的定义:

    public interface IAuthenticationService
    {
        Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties);
    }
    

    有了接口,肯定有实现咯。 我们找一下实现在哪里,很容易,根据 ASP.NET Core 的 IOC 来找就行了,很明显在 AddCookie 这个扩展里面。

    public void ConfigureServices(IServiceCollection services)
    {
        ...
        
        services.AddAuthentication()
            .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme); 
             ↑↑↑ 实现就在这里
        ...
    } 
    

    我们找到了处理类 CookieAuthenticationHandler 这个对象,我们再来看具体的代码。

    protected async override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
    {
        // Process the request cookie to initialize members like _sessionKey.
        await EnsureCookieTicket();
        var cookieOptions = BuildCookieOptions();
    
        var signInContext = new CookieSigningInContext(
            Context,
            Scheme,
            Options,
            user,
            properties,
            cookieOptions);
    
        await Events.SigningIn(signInContext);
    
        var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.Scheme.Name);
    
        if (Options.SessionStore != null)
        {
            if (_sessionKey != null)
            {
                await Options.SessionStore.RemoveAsync(_sessionKey);
            }
            _sessionKey = await Options.SessionStore.StoreAsync(ticket);
            var principal = new ClaimsPrincipal(
                new ClaimsIdentity(
                    new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
                    Options.ClaimsIssuer));
            ticket = new AuthenticationTicket(principal, null, Scheme.Name);
        }
    
        var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());
    
        Options.CookieManager.AppendResponseCookie(
            Context,
            Options.Cookie.Name,
            cookieValue,
            signInContext.CookieOptions);
    
        var signedInContext = new CookieSignedInContext(
            Context,
            Scheme,
            signInContext.Principal,
            signInContext.Properties,
            Options);
    
        await Events.SignedIn(signedInContext);
    
        // Only redirect on the login path
        var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath;
        await ApplyHeaders(shouldRedirect, signedInContext.Properties);
    
        Logger.SignedIn(Scheme.Name);
    }
            
    

    大概步骤分为:

    1、创建一个SignIn Cookie 上下文对象
    2、将上下文对象转换为票据(Ticket),转换为票据的目的是为了加密
    3、将票据进行加密
    4、将加密后的票据写入Cookie

    很有意思的是第三步,我需要展开来说下,这也结束。

    在第三步加密票据的过程中可以看到有一个 if 判断 if (Options.SessionStore != null),是做什么用的呢?

    可能有些同学会有疑问,我们基于Claim的Cookie存储假如我的证件单元很多,就会生成一个非常大的cookie,每次传输是有性能影响的,并且Cookie是有最大限制的,怎么办呢?

    其实解决办法就是我们就可以开启这个 SessionStore,将Cookie存储在服务端例如Redis等缓存中。代码如下:

    services.AddSingleton<ITicketStore, MyRedisTicketStore>();
    
    services.AddOptions<CookieAuthenticationOptions>("Cookies")
         .Configure<ITicketStore>((o, t) => o.SessionStore = t);
    

    现在,浏览器中已经存储了用户的身份啦。

    以上就是确认用户身份的一个过程,在这个过程中我们使用Cookie来标记用户身份并且存储到浏览器的Cookie了,这个过程就是 认证

    其实上面就是 ASP.NET Core 中的 Forms 身份验证中的认证阶段。

    扩展阅读

    在不使用Cookie的时候怎么确定身份呢? 比如在 WEB API 接口中使用的就是 Access Token,这也相当于Cookie中的票据了,那么在 WEB API 中如何确定身份,流程又是怎么样的呢?可以看后续文章。

    总结

    才把认证写完发现已经这么长了,下篇再来讲讲授权吧。

    如果你对 .NET Core 有兴趣的话可以关注我,我会定期的在博客分享我的学习心得。


    本文地址:http://www.cnblogs.com/savorboard/p/authentication.html
    作者博客:Savorboard
    本文原创授权为:署名 - 非商业性使用 - 禁止演绎,协议普通文本 | 协议法律文本

  • 相关阅读:
    使用PaintCode便捷地实现动画效果
    程序员常用markdown语法记忆小结之博客园markdown编辑器的效果
    kafka-重复消费-1
    nosql
    ThreadLocal
    内存溢出、内存泄漏
    springboot邮件服务
    三次握手、四次挥手
    悲观锁乐观锁简单整理
    beanstalkd
  • 原文地址:https://www.cnblogs.com/savorboard/p/authentication.html
Copyright © 2020-2023  润新知