查看原文
其他

ASP.NET Core 身份验证(一)

DotNet 2019-08-01

(给DotNet加星标,提升.Net技能


转自:Savorboard

cnblogs.com/savorboard/p/authentication.html


前言


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


Identity 入门系列文章:


  • Identity 入门一(https://www.cnblogs.com/savorboard/p/aspnetcore-identity.html)


  • Identity 入门二(https://www.cnblogs.com/savorboard/p/aspnetcore-identity2.html)


  • Identity 入门三(https://www.cnblogs.com/savorboard/p/aspnetcore-identity3.html)


名词解释


做 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 中如何确定身份,流程又是怎么样的呢?可以看后续文章。


总结


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


推荐阅读

(点击标题可跳转阅读)

让你的ASP.NET Core应用程序更安全

.NET Core 迁移躺坑记

深入源码理解.NET Core中Startup的注册及运行


看完本文有收获?请转发分享给更多人

关注「DotNet」加星标,提升.Net技能 

喜欢就点一下「在看」呗~

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存