.net core 认证 Authentication 授权 Authorization 补充篇 (1)
Tips:本篇已加入,.Net core 3.1 使用IdentityServer4 实现 OAuth2.0 --阅读目录 可点击查看更多相关文章。
前言
接着上一篇 ID4 客户端模式(Client Credentials Grant 的实现,我们会发现运用起来好像很方遍,但是结尾的那些思考又有多少同学能讲的明白。
不知道大家还记不记得之前 IdentityServer4 介绍以及一些名词解释 文章最后有大致介绍 Authentication 和 Authorization 怎么区分,他们到底做了什么事情。
这篇文章我会深入刨析 Authentication 模型和原理。
回顾
我们先简单的回顾一下之前文章中,我对 认证和授权 的理解:
我们做过这样一个比喻,新员工第一天去公司报到,前台给员工发了门禁卡,那么发给你门禁卡之前,肯定已经从人事那边确认了,这是我们的新同事,那么发门禁卡就相当于认证Authentication(识别身份),
我们拿着门禁卡刷开自己的办公室,这一步动作就是 授权Authorization(验证这个门禁卡有没有刷自己办公室的权限),
如果你用自己的门禁卡刷了财务办公室,发现刷不开,这就是授权失败(财务办公室级别比较高一般员工的门禁卡没有这个权限)。
这个门禁卡就相当于授权中心颁发的认证票据(.netcore 中称这种令牌为 Authentication Ticket 认证票据)
当你离职的时候前台会回收门禁卡,你将无法进入公司(这就相当于 票据的回收 Ticket Revoker)
你们看这个前台是不是就像我们的认证服务器,公司是不是就像我们要访问的资源。
AuthenticationMiddleware中间件
要完全理解 认证,需要深入的刨析这个认证中间件
还记得 上一篇ID4 客户端授权模式 资源api输出的 User.Claims 吗?我们从他开始说起。
如果你有一些授权认证的开发经验,其实不难从字面上看出 User.Claims其实就是 这个用户的声明(声明就是 很多信息的组合 比如:姓名 ,邮箱,等等...)
我们通过 F12看到 ,这个User.Claims 的其实就是 ControllerBase 基类中的 一个属性 (ClaimsPrincipal)User 里的子属性, 获取 这个用户的 所有Claims
(微软的模型中如何已经通过认证那么管道里就会有认证的一系列相关数据,这些数据可以让我们知道认证当时一些情况,并且他也会成为之后授权的一些参数)
那么开始第一个知识点 我们看看这个User里到底有些什么。
用户身份
有三个对象十分重要,ClaimsIdentity(这个是身份最终的产出结果),IIdentity(这个是最基础的),Claim(这个是明细项)
上图描述了 三个类型的关系,ClaimsIdentity 可以理解为最终的身份最小单元。他的属性IsAuthenticated 取决于 是否有明确的 认证类型。
还有两个类 IPrincipal 和 ClaimsPrincipal ,我们可以理解为 最终用户,ClaimsPrincipal用户可以有很多身份 ,我们看下图描述的关系:
我们用一段代码来总结一下我们这块内容,大家可以试着操作一下,然后看看对象的监视器里有哪些属性,值分别是什么。
var identity1 = new ClaimsIdentity(authenticationType: "AT1", claims: new Claim[] { new Claim(ClaimTypes.Name, "Benjamin") }); var identity2 = new ClaimsIdentity(authenticationType: "AT2", claims: new Claim[] { new Claim(ClaimTypes.Name, "Benjamin2") }); var claimPrincipal = new ClaimsPrincipal(new ClaimsIdentity[] { identity1, identity2 }); var claimPrincipal2 = new ClaimsPrincipal(identity1);
我们再 切换到之前的 ID4 实现的案例,我们看一下ID4 客户端授权模式下 最终体现出来的用户和身份是啥样子的
这个用户里面有一个identity 一对一的关系,再看一下认证的类型是 AuthenticationTypes.Federation ,并且有8个声明
{nbf: 1601621754}
{exp: 1601625354}
{iss: http://localhost:5000}
{aud: api1}
{client_id: client}
{jti: 36D4DF4DED77F00BC6EC52914B11D33B}
{iat: 1601621754}
{scope: api1}
nbf,jti,iat都是JWT的 一些特有属性这个之后专门为大家讲解。
我们从结果已经能透彻的分析出 认证的内容了,那么认证内部是怎么处理请求的呢?我们接着往下看,微软的认证都是基于票据的,那么还有一个知识点必不可少那就是认证票据
AuthenticationTicket
我们最终颁发出去,并且认证的时候看的都是票据,票据具体会放哪些内容,怎么加密呢,我们看一下下面这张图
认证票据是一个带有方案名描述的用户身份类,并且还会带有很多认证需要的信息,这个标签通过TickerDataFormat进行格式化
格式化内容包括序列化和加密都是通过SecureDataFormat基类继承,其中序列化通过 TicketSerializer完成,加密需要自定义实现IDataProtector
身份用户也有了,认证票据也颁发出来了,那么接下来看一下一个核心处理接口:IAuthenticationHandler
认证的核心
为了更形象的表述一个请求时怎么通过验证或者被拒绝的 有这3个类起了很关键的作用,源码地址:https://github.com/aspnet/Security
1. IAuthenticationHandler
AuthenticateAsync 是认证的核心方法
ChallengeAsync 作用是返回是否 401未认证
ForbidAsync 作用返回403 无权限
InitializeAsync 完成一些初始话工作
最终处理完的认证会返回 AuthenticateResult 具体内容如下:
2.IAuthenticationHandlerProvider
认证处理的适配器(我是这么翻译的)
我们可以自定义很多类 去实现IAuthenticationHandler接口,那么由谁去最终选择呢,那无疑就是这个认证处理的适配器了
这个适配器就提供一个方法
我们可以根据认证的方案名 去具体讲各个请求转到不同的Handler的实现里去。
那么问题又来了,方案名我们怎么注册进去那就要看最后一个类了 AuthenticationSchemeProvider
3.AuthenticationSchemeProvider
方案名的注册 我们看一下这个代码:
.AddAuthentication(options => { options.DefaultScheme = "BenBearer"; })
AuthorizationOptions的注册后 AuthenticationSchemeProvider就有了一张完整的方案名与 AuthenticationScheme的关系,
那之后就可以通过 实现 IAuthenticationSchemeProvider的接口,
核心操作是如何被调用的
从上面的核心那段介绍可以看出 IAuthenticationHandler 的实现是最终的处理核心,那么netcore 从什么地方触发这些核心处理呢,
那么这个类就很关键了,IAuthenticationService这个类的实现完成了
此块内容源码地址 https://github.com/aspnet/HttpAbstractions
最终AuthenticationService源码如下:
namespace Microsoft.AspNetCore.Authentication { /// <summary> /// Implements <see cref="IAuthenticationService"/>. /// </summary> public class AuthenticationService : IAuthenticationService { /// <summary> /// Constructor. /// </summary> /// <param name="schemes">The <see cref="IAuthenticationSchemeProvider"/>.</param> /// <param name="handlers">The <see cref="IAuthenticationRequestHandler"/>.</param> /// <param name="transform">The <see cref="IClaimsTransformation"/>.</param> public AuthenticationService(IAuthenticationSchemeProvider schemes, IAuthenticationHandlerProvider handlers, IClaimsTransformation transform) { Schemes = schemes; Handlers = handlers; Transform = transform; } /// <summary> /// Used to lookup AuthenticationSchemes. /// </summary> public IAuthenticationSchemeProvider Schemes { get; } /// <summary> /// Used to resolve IAuthenticationHandler instances. /// </summary> public IAuthenticationHandlerProvider Handlers { get; } /// <summary> /// Used for claims transformation. /// </summary> public IClaimsTransformation Transform { get; } /// <summary> /// Authenticate for the specified authentication scheme. /// </summary> /// <param name="context">The <see cref="HttpContext"/>.</param> /// <param name="scheme">The name of the authentication scheme.</param> /// <returns>The result.</returns> public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme) { if (scheme == null) { var defaultScheme = await Schemes.GetDefaultAuthenticateSchemeAsync(); scheme = defaultScheme?.Name; if (scheme == null) { throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found."); } } var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { throw await CreateMissingHandlerException(scheme); } var result = await handler.AuthenticateAsync(); if (result != null && result.Succeeded) { var transformed = await Transform.TransformAsync(result.Principal); return AuthenticateResult.Success(new AuthenticationTicket(transformed, result.Properties, result.Ticket.AuthenticationScheme)); } return result; } /// <summary> /// Challenge the specified authentication scheme. /// </summary> /// <param name="context">The <see cref="HttpContext"/>.</param> /// <param name="scheme">The name of the authentication scheme.</param> /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> /// <returns>A task.</returns> public virtual async Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties) { if (scheme == null) { var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync(); scheme = defaultChallengeScheme?.Name; if (scheme == null) { throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found."); } } var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { throw await CreateMissingHandlerException(scheme); } await handler.ChallengeAsync(properties); } /// <summary> /// Forbid the specified authentication scheme. /// </summary> /// <param name="context">The <see cref="HttpContext"/>.</param> /// <param name="scheme">The name of the authentication scheme.</param> /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> /// <returns>A task.</returns> public virtual async Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties) { if (scheme == null) { var defaultForbidScheme = await Schemes.GetDefaultForbidSchemeAsync(); scheme = defaultForbidScheme?.Name; if (scheme == null) { throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultForbidScheme found."); } } var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { throw await CreateMissingHandlerException(scheme); } await handler.ForbidAsync(properties); } /// <summary> /// Sign a principal in for the specified authentication scheme. /// </summary> /// <param name="context">The <see cref="HttpContext"/>.</param> /// <param name="scheme">The name of the authentication scheme.</param> /// <param name="principal">The <see cref="ClaimsPrincipal"/> to sign in.</param> /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> /// <returns>A task.</returns> public virtual async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (scheme == null) { var defaultScheme = await Schemes.GetDefaultSignInSchemeAsync(); scheme = defaultScheme?.Name; if (scheme == null) { throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignInScheme found."); } } var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { throw await CreateMissingSignInHandlerException(scheme); } var signInHandler = handler as IAuthenticationSignInHandler; if (signInHandler == null) { throw await CreateMismatchedSignInHandlerException(scheme, handler); } await signInHandler.SignInAsync(principal, properties); } /// <summary> /// Sign out the specified authentication scheme. /// </summary> /// <param name="context">The <see cref="HttpContext"/>.</param> /// <param name="scheme">The name of the authentication scheme.</param> /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> /// <returns>A task.</returns> public virtual async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties) { if (scheme == null) { var defaultScheme = await Schemes.GetDefaultSignOutSchemeAsync(); scheme = defaultScheme?.Name; if (scheme == null) { throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignOutScheme found."); } } var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { throw await CreateMissingSignOutHandlerException(scheme); } var signOutHandler = handler as IAuthenticationSignOutHandler; if (signOutHandler == null) { throw await CreateMismatchedSignOutHandlerException(scheme, handler); } await signOutHandler.SignOutAsync(properties); } private async Task<Exception> CreateMissingHandlerException(string scheme) { var schemes = string.Join(", ", (await Schemes.GetAllSchemesAsync()).Select(sch => sch.Name)); var footer = $" Did you forget to call AddAuthentication().Add[SomeAuthHandler](\"{scheme}\",...)?"; if (string.IsNullOrEmpty(schemes)) { return new InvalidOperationException( $"No authentication handlers are registered." + footer); } return new InvalidOperationException( $"No authentication handler is registered for the scheme '{scheme}'. The registered schemes are: {schemes}." + footer); } private async Task<string> GetAllSignInSchemeNames() { return string.Join(", ", (await Schemes.GetAllSchemesAsync()) .Where(sch => typeof(IAuthenticationSignInHandler).IsAssignableFrom(sch.HandlerType)) .Select(sch => sch.Name)); } private async Task<Exception> CreateMissingSignInHandlerException(string scheme) { var schemes = await GetAllSignInSchemeNames(); // CookieAuth is the only implementation of sign-in. var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?"; if (string.IsNullOrEmpty(schemes)) { return new InvalidOperationException( $"No sign-in authentication handlers are registered." + footer); } return new InvalidOperationException( $"No sign-in authentication handler is registered for the scheme '{scheme}'. The registered sign-in schemes are: {schemes}." + footer); } private async Task<Exception> CreateMismatchedSignInHandlerException(string scheme, IAuthenticationHandler handler) { var schemes = await GetAllSignInSchemeNames(); var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for SignInAsync. "; if (string.IsNullOrEmpty(schemes)) { // CookieAuth is the only implementation of sign-in. return new InvalidOperationException(mismatchError + $"Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and SignInAsync(\"Cookies\",...)?"); } return new InvalidOperationException(mismatchError + $"The registered sign-in schemes are: {schemes}."); } private async Task<string> GetAllSignOutSchemeNames() { return string.Join(", ", (await Schemes.GetAllSchemesAsync()) .Where(sch => typeof(IAuthenticationSignOutHandler).IsAssignableFrom(sch.HandlerType)) .Select(sch => sch.Name)); } private async Task<Exception> CreateMissingSignOutHandlerException(string scheme) { var schemes = await GetAllSignOutSchemeNames(); var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?"; if (string.IsNullOrEmpty(schemes)) { // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it. return new InvalidOperationException($"No sign-out authentication handlers are registered." + footer); } return new InvalidOperationException( $"No sign-out authentication handler is registered for the scheme '{scheme}'. The registered sign-out schemes are: {schemes}." + footer); } private async Task<Exception> CreateMismatchedSignOutHandlerException(string scheme, IAuthenticationHandler handler) { var schemes = await GetAllSignOutSchemeNames(); var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for {nameof(SignOutAsync)}. "; if (string.IsNullOrEmpty(schemes)) { // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it. return new InvalidOperationException(mismatchError + $"Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and {nameof(SignOutAsync)}(\"Cookies\",...)?"); } return new InvalidOperationException(mismatchError + $"The registered sign-out schemes are: {schemes}."); } } }
里面有我们上一段落的所有涉及到的对象会注入,
然后HttpContext的上下文中,会有很多扩展方法进行认证这块的操作
AuthenticationHttpContextExtensions。
这块的源码地址:https://github.com/aspnet/HttpAbstractions/blob/master/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationHttpContextExtensions.cs
服务的注册
最终服务的注册中会把 IAuthenticaitonServer的实现 AuthenticaitonServer ,IAuthenticationHandlerProvider的实现,IAuthenticationSchemeProvider的实现注入进去。
最后 认证的中间件AuthenticationMiddleware所作的是事情就是当请求路过这个中间件的时候,拦截处理返回,并将一些信息附加到HttpContext上。
是不是感觉也没有那么复杂那么难。
源码分析不是很多,但是Authencation认证这块基本流程就是基本上述这么一些,分析可能不是很到位,但是希望可以给大家一些启发。
如果觉得本篇随笔对你有帮助,请点击右下方的 【推荐👍】,或者给作者 【打赏】,感谢大家的支持,这将成为作者继续写作的动力 |