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
本文原创授权为:署名 - 非商业性使用 - 禁止演绎,协议普通文本 | 协议法律文本

posted @ 2019-04-23 09:09 Savorboard 阅读(...) 评论(...) 编辑 收藏