.Net 5.0 通过IdentityServer4实现单点登录之授权部分源码解析

1、本文主要介绍.Net 5.0通过认证授权、路由终结点、OpenIdConnect组件结合IdentityServer4实现单点登录的源码解析,内容较多,只解读demo的调用部分.

首先StartUp相关代码:

    public class Startup
    {
       
        public void ConfigureServices(IServiceCollection services)
        {
            //注入控制器相关服务到容器
            services.AddControllersWithViews();
            JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies")
            .AddOpenIdConnect("oidc", options =>
            {
                options.Authority = "http://localhost:5001";
                options.RequireHttpsMetadata = false;
                options.ClientId = "mvc";
                options.ClientSecret = "secret";
                options.ResponseType = "code";
                options.SaveTokens = true;
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
       //路由中间件 app.UseRouting();
       //认证中间件 app.UseAuthentication();
//授权中间件 app.UseAuthorization()
;
//终结点 app.UseEndpoints(endpoints
=> { endpoints.MapDefaultControllerRoute() .RequireAuthorization(); }); } }

首先看ConfigureServices方法,通过该方法注入将控制器注入到了容器中,并配置认证组件的默认认证方案为Cookie,Challenge方案设置为oidc,其实就是当用户未认证时,回调用oidc方法的Handler,下面会解释,接着看Configure方法,启用路由、认证、授权、终结点中间价,并在终结点里面配置了给默认控制器路由全都设置了Authorize特性.相当于所有的控制器方法,必要要登录过后才能访问.下面会进行源码解析.

 

2、授权中间件源码解析

再通过上述代码配置好客户端之后,说明客户端已经具备接入oidc服务端了(本文不多做讲解),那么现在访问客户端api,必然会被拦截,应为在配置客户端时,引入了授权组件,并且给所有的控制器方法加上了Authorize特性.相当于所有的控制器方法,必要要登录过后才能访问.ok,带着这个前提条件,来看看授权中间件(如果不了解授权中间件请参阅.Net Core 3.0授权组件源码解析)做了什么.

            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            var endpoint = context.GetEndpoint();
            if (endpoint != null)
            {
                
            }

            // IMPORTANT: Changes to authorization logic should be mirrored in MVC's AuthorizeFilter
            var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();

首先先在当前访问的终结点获取到其Metadata中关于授权的信息细节如下:

    public interface IAuthorizeData
    {
        /// <summary>
        /// 终结点(可以理解为控制器方法)配置的认证策略
        /// </summary>
        string? Policy { get; set; }

        /// <summary>
        /// 终结点(可以理解为控制器方法)配置的角色(大多数系统是基于Role的授权策略)
        /// </summary>
        string? Roles { get; set; }

        /// <summary>
        /// 终结点(可以理解为控制器方法)配置的认证方式
        /// </summary>
        string? AuthenticationSchemes { get; set; }
    }

细节就是获取控制器方法上的实现IAuthorizeData(默认Authorize特性)的特性内容,包括方法采用的认证方案、采用的自定义授权策略、采用的角色授权策略信息.获取当上述信息后,接下看源码如下:

            var policy = await AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData);
            if (policy == null)
            {
                await _next(context);
                return;
            }

判断一下控制器方法上的Authorize特性中的授权策略相关的内容是否为空,为空的话,直接执行接下去的中间件.这里查阅下授权策略是如何Combine的,代码如下:

       public static async Task<AuthorizationPolicy?> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData)
        {
            if (policyProvider == null)
            {
                throw new ArgumentNullException(nameof(policyProvider));
            }

            if (authorizeData == null)
            {
                throw new ArgumentNullException(nameof(authorizeData));
            }

            // Avoid allocating enumerator if the data is known to be empty
            var skipEnumeratingData = false;
            if (authorizeData is IList<IAuthorizeData> dataList)
            {
                skipEnumeratingData = dataList.Count == 0;
            }

            AuthorizationPolicyBuilder? policyBuilder = null;

            //如果控制器方法设置了Authorize特性
            if (!skipEnumeratingData)
            {
                //遍历控制器方法的Authorize特性
                foreach (var authorizeDatum in authorizeData)
                {
                    if (policyBuilder == null)
                    {
                        policyBuilder = new AuthorizationPolicyBuilder();
                    }

                    var useDefaultPolicy = true;


                    /*
                     * 主要功能:如果控制器方法打了Authorize特性,且Policy自定义策略有值,则从配置中获取自定义策略,如果配置中存在,则将值写入到AuthorizationPolicyBuilder实例中
                     * */

                    //如果自定义策略不为空
                    if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy))
                    {
                        //如果没有重写IAuthorizationPolicyProvider,默认从AuthorizationOptions配置中获取自定义授权策略
                        //增加自定义授权策略通过配置AuthorizationOptions来增加,具体通过AuthorizationOptions的AddPolicy方法,来添加自定义授权策略.
                        var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy);
                        if (policy == null)
                        {
                            throw new InvalidOperationException("");
                        }

                        //将从配置中的自定义授权策略中获取到的认证方案集合和Requirements集合转换成AuthorizationPolicyBuilder实例
                        policyBuilder.Combine(policy);
                        useDefaultPolicy = false;
                    }

                    //判断角色授权策略是否为空
                    var rolesSplit = authorizeDatum.Roles?.Split(',');
                    if (rolesSplit?.Length > 0)
                    {
                        //将角色策略集合添加到AuthorizationPolicyBuilder实例中的Requirements属性中
                        var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
                        policyBuilder.RequireRole(trimmedRolesSplit);
                        useDefaultPolicy = false;
                    }

                    //判断认证方案是否为空
                    var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(',');
                    if (authTypesSplit?.Length > 0)
                    {
                        //将认证方案写入到AuthorizationPolicyBuilder实例中的AuthenticationSchemes属性中
                        foreach (var authType in authTypesSplit)
                        {
                            if (!string.IsNullOrWhiteSpace(authType))
                            {
                                policyBuilder.AuthenticationSchemes.Add(authType.Trim());
                            }
                        }
                    }

                    //如果控制器方法没有指定任何的自定义策略、角色策略、认证方案
                    if (useDefaultPolicy)
                    {
                        //采用默认的AuthorizationOptions配置的默认策略.接下去的文章会介绍
                        policyBuilder.Combine(await policyProvider.GetDefaultPolicyAsync());
                    }
                }
            }

            // 如果没有任何策略,则采用Fallback策略
            if (policyBuilder == null)
            {
                var fallbackPolicy = await policyProvider.GetFallbackPolicyAsync();
                if (fallbackPolicy != null)
                {
                    return fallbackPolicy;
                }
            }

            //将policyBuilder转换成police实例返回
            return policyBuilder?.Build();
        }

ok,到这里上面的代码里面都有注释,总结一下,这个方法主要是为了将当前控制方法打的Authorize特性中指定的认证方案、自定义授权策略(会根据自定义授权策略名称,从配置中获取具体的配置内容,前提是没有重写IAuthorizationPolicyProvider)、角色授权策略.最后转换成AuthorizationPolicy实例返回, 实例中包含当前控制器方法采用的认证方案、其要求的AuthorizationRequirement集合.

注意:如果控制器方法没有打Authorize特性,而是通过如下代码:

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute()
                    .RequireAuthorization();
            });

通过给终结点配置RequireAuthorization的方式,如下代码:

        public static TBuilder RequireAuthorization<TBuilder>(this TBuilder builder, params string[] policyNames) where TBuilder : IEndpointConventionBuilder
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            if (policyNames == null)
            {
                throw new ArgumentNullException(nameof(policyNames));
            }

            return builder.RequireAuthorization(policyNames.Select(n => new AuthorizeAttribute(n)).ToArray());
        }

        private static void RequireAuthorizationCore<TBuilder>(TBuilder builder, IEnumerable<IAuthorizeData> authorizeData)
            where TBuilder : IEndpointConventionBuilder
        {
            builder.Add(endpointBuilder =>
            {
                foreach (var data in authorizeData)
                {
                    endpointBuilder.Metadata.Add(data);
                }
            });
        }

通过这种方式,会自动给终结点的Metdata加上Authorize特性,但是不会指定任何信息.所以进入授权中间件调用AuthorizationPolicy.CombineAsync会去读取配置中的默认授权策略.代码如下:

 public AuthorizationPolicy DefaultPolicy { get; set; } = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
        public AuthorizationPolicyBuilder RequireAuthenticatedUser()
        {
            Requirements.Add(new DenyAnonymousAuthorizationRequirement());
            return this;
        }
public class DenyAnonymousAuthorizationRequirement : AuthorizationHandler<DenyAnonymousAuthorizationRequirement>, IAuthorizationRequirement
    {
        /// <summary>
        /// Makes a decision if authorization is allowed based on a specific requirement.
        /// </summary>
        /// <param name="context">The authorization context.</param>
        /// <param name="requirement">The requirement to evaluate.</param>
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DenyAnonymousAuthorizationRequirement requirement)
        {
            var user = context.User;
            var userIsAnonymous =
                user?.Identity == null ||
                !user.Identities.Any(i => i.IsAuthenticated);
            if (!userIsAnonymous)
            {
                context.Succeed(requirement);
            }
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public override string ToString()
        {
            return $"{nameof(DenyAnonymousAuthorizationRequirement)}: Requires an authenticated user.";
        }
    }

默认授权策略,很简单,判断当前上下文中的用户有没有登录.说明在终结点中配置endpoints.MapDefaultControllerRoute().RequireAuthorization().说明所有的api访问,当前上下文中的用户必须处于登录状态.

如果没有在终结点中配置endpoints.MapDefaultControllerRoute().RequireAuthorization(),那么可以尝试在AuthorizationOptions配置FallbackPolicy来实现回退策略.

 

ok,接着看中间件的源码,通过上面的代码拿到了当前控制器方法的,授权策略实例,包含认证方案、及要求的AuthorizationRequirement集合.接着执行如下代码:

            if (policy == null)
            {
                await _next(context);
                return;
            }

            // Policy evaluator has transient lifetime so it fetched from request services instead of injecting in constructor
            var policyEvaluator = context.RequestServices.GetRequiredService<IPolicyEvaluator>();

            var authenticateResult = await policyEvaluator.AuthenticateAsync(policy, context);

如果没有在终结点中配置endpoints.MapDefaultControllerRoute().RequireAuthorization()且没有配置FallbackPolicy来实现回退策略,那么直接跳过,执行接下去的中间件.

 接着执行IPolicyEvaluator中的逻辑代码如下:

        public virtual async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
        {
            if (policy.AuthenticationSchemes != null && policy.AuthenticationSchemes.Count > 0)
            {
                ClaimsPrincipal? newPrincipal = null;
                foreach (var scheme in policy.AuthenticationSchemes)
                {
                    var result = await context.AuthenticateAsync(scheme);
                    if (result != null && result.Succeeded)
                    {
                        
                        newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result.Principal);
                    }
                }

                if (newPrincipal != null)
                {
                    context.User = newPrincipal;
                    return AuthenticateResult.Success(new AuthenticationTicket(newPrincipal, string.Join(";", policy.AuthenticationSchemes)));
                }
                else
                {
                    context.User = new ClaimsPrincipal(new ClaimsIdentity());
                    return AuthenticateResult.NoResult();
                }
            }

            return (context.User?.Identity?.IsAuthenticated ?? false)
                ? AuthenticateResult.Success(new AuthenticationTicket(context.User, "context.User"))
                : AuthenticateResult.NoResult();
        }

这里就很简单了,先判断控制器方法中有没有特别执行认证方案,如果指定了,那么就去执行该认证方案的Handler,demo中没有给控制器方法指定任何认证方案,所以结果返回必然是AuthenticateResult.NoResult(),内容如下图:

 

 ok,接着看中间价源码:

            if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null)
            {
                await _next(context);
                return;
            }

这里很简答了,如果当前访问的控制器方法打了AllowAnonymous特性,那不管上面的中间件发生了什么直接执行接下去的中间件.

接着看中间件代码如下:

            object? resource;
            if (AppContext.TryGetSwitch(SuppressUseHttpContextAsAuthorizationResource, out var useEndpointAsResource) && useEndpointAsResource)
            {
                resource = endpoint;
            }
            else
            {
                resource = context;
            }

这里,我看懂,但是不影响整体,后续有时间,研究下,这里不做讲解.

接着看中间件代码如下:

 var authorizeResult = await policyEvaluator.AuthorizeAsync(policy, authenticateResult, context, resource);

 接着看实现,如下:

        public virtual async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object? resource)
        {
            if (policy == null)
            {
                throw new ArgumentNullException(nameof(policy));
            }

            var result = await _authorization.AuthorizeAsync(context.User, resource, policy);
            if (result.Succeeded)
            {
                return PolicyAuthorizationResult.Success();
            }

            // If authentication was successful, return forbidden, otherwise challenge
            return (authenticationResult.Succeeded)
                ? PolicyAuthorizationResult.Forbid(result.Failure)
                : PolicyAuthorizationResult.Challenge();
        }

重点看

  var result = await _authorization.AuthorizeAsync(context.User, resource, policy);

接着看具体实现,如下:

        public virtual async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, IEnumerable<IAuthorizationRequirement> requirements)
        {
            if (requirements == null)
            {
                throw new ArgumentNullException(nameof(requirements));
            }

            var authContext = _contextFactory.CreateContext(requirements, user, resource);
            var handlers = await _handlers.GetHandlersAsync(authContext);
            foreach (var handler in handlers)
            {
                await handler.HandleAsync(authContext);
                if (!_options.InvokeHandlersAfterFailure && authContext.HasFailed)
                {
                    break;
                }
            }

            var result = _evaluator.Evaluate(authContext);
            if (result.Succeeded)
            {
                _logger.UserAuthorizationSucceeded();
            }
            else
            {
                _logger.UserAuthorizationFailed(result.Failure!);
            }
            return result;
        }

注:这里的rescource是空.调用传入未空.

简单的参数校验,接着创建一个auth上下文(根据AuthorizationPolicy实例中的RequireMents和resorce和当前上下文的User(这些信息实际是从当前httpcontext和当前终结点的Authrize特性中解析获得)),大致的内容如下:

        public AuthorizationHandlerContext(
            IEnumerable<IAuthorizationRequirement> requirements,
            ClaimsPrincipal user,
            object? resource)
        {
            if (requirements == null)
            {
                throw new ArgumentNullException(nameof(requirements));
            }

            Requirements = requirements;
            User = user;
            Resource = resource;
            _pendingRequirements = new HashSet<IAuthorizationRequirement>(requirements);
        }

创建了一个AuthorizationHandlerContext上下文实例,接着看如下代码:

var handlers = await _handlers.GetHandlersAsync(authContext);

实现如下代码:

    public class DefaultAuthorizationHandlerProvider : IAuthorizationHandlerProvider
    {
        private readonly IEnumerable<IAuthorizationHandler> _handlers;

        /// <summary>
        /// Creates a new instance of <see cref="DefaultAuthorizationHandlerProvider"/>.
        /// </summary>
        /// <param name="handlers">The <see cref="IAuthorizationHandler"/>s.</param>
        public DefaultAuthorizationHandlerProvider(IEnumerable<IAuthorizationHandler> handlers)
        {
            if (handlers == null)
            {
                throw new ArgumentNullException(nameof(handlers));
            }

            _handlers = handlers;
        }

        /// <inheritdoc />
        public Task<IEnumerable<IAuthorizationHandler>> GetHandlersAsync(AuthorizationHandlerContext context)
            => Task.FromResult(_handlers);
    }

从源码分析,从容器中获取所有的IAuthorizationHandler实例,默认实现是根据AuthorizationHandlerContext上下文实例获取的,但是提供的默认实现没有任何逻辑,所以这里可以重构,提升获取对应的Handler实例的效率.接着看源码:

            foreach (var handler in handlers)
            {
                await handler.HandleAsync(authContext);
                if (!_options.InvokeHandlersAfterFailure && authContext.HasFailed)
                {
                    break;
                }
            }

这里,就是便利所有的注入的handler实例,来处理认AuthorizationHandlerContext上下文实例,接着看handler抽象如下代码:

    public abstract class AuthorizationHandler<TRequirement> : IAuthorizationHandler
            where TRequirement : IAuthorizationRequirement
    {
        /// <summary>
        /// Makes a decision if authorization is allowed.
        /// </summary>
        /// <param name="context">The authorization context.</param>
        public virtual async Task HandleAsync(AuthorizationHandlerContext context)
        {
            foreach (var req in context.Requirements.OfType<TRequirement>())
            {
                await HandleRequirementAsync(context, req);
            }
        }

        /// <summary>
        /// Makes a decision if authorization is allowed based on a specific requirement.
        /// </summary>
        /// <param name="context">The authorization context.</param>
        /// <param name="requirement">The requirement to evaluate.</param>
        protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement);
    }

这里很简单,取出auth上下文中的Requirements属性,调用子类实现,这里介绍一个典型的子类实现:

public class DenyAnonymousAuthorizationRequirement : AuthorizationHandler<DenyAnonymousAuthorizationRequirement>, IAuthorizationRequirement
    {
        /// <summary>
        /// Makes a decision if authorization is allowed based on a specific requirement.
        /// </summary>
        /// <param name="context">The authorization context.</param>
        /// <param name="requirement">The requirement to evaluate.</param>
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DenyAnonymousAuthorizationRequirement requirement)
        {
            var user = context.User;
            var userIsAnonymous =
                user?.Identity == null ||
                !user.Identities.Any(i => i.IsAuthenticated);
            if (!userIsAnonymous)
            {
                context.Succeed(requirement);
            }
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public override string ToString()
        {
            return $"{nameof(DenyAnonymousAuthorizationRequirement)}: Requires an authenticated user.";
        }
    }

这里判断了当前httpcontext中的用户相关属性是否认证通过.如果认证通过设置AuthorizationHandlerContext实例的_succeedCalled字段未true.并移除AuthorizationHandlerContext实例的Requirements的HashSet副本中的传入Requirements.到这里回到之前的源码:

            foreach (var handler in handlers)
            {
                await handler.HandleAsync(authContext);
                if (!_options.InvokeHandlersAfterFailure && authContext.HasFailed)
                {
                    break;
                }
            }

这里很简单,如果给授权组件的参数AuthorizationOptions的InvokeHandlersAfterFailure属性设为false,且当前授权失败的情况下,剩余授权处理器任然能正常执行,反之,则不.

接着看源码:

            var result = _evaluator.Evaluate(authContext);
            if (result.Succeeded)
            {
                _logger.UserAuthorizationSucceeded();
            }
            else
            {
                _logger.UserAuthorizationFailed(result.Failure!);
            }
            return result;
    public class DefaultAuthorizationEvaluator : IAuthorizationEvaluator
    {
        /// <summary>
        /// Determines whether the authorization result was successful or not.
        /// </summary>
        /// <param name="context">The authorization information.</param>
        /// <returns>The <see cref="AuthorizationResult"/>.</returns>
        public AuthorizationResult Evaluate(AuthorizationHandlerContext context)
            => context.HasSucceeded
                ? AuthorizationResult.Success()
                : AuthorizationResult.Failed(context.HasFailed
                    ? AuthorizationFailure.ExplicitFail()
                    : AuthorizationFailure.Failed(context.PendingRequirements));
    }

这里就很简单了,判断下AuthorizationHandlerContext上下文实例是否登录成功.通过调用其HasSuccessed属性,其代码如下:

        public virtual bool HasSucceeded
        {
            get
            {
                return !_failCalled && _succeedCalled && !PendingRequirements.Any();
            }
        }

结合上面的遍历所有Handler实例的代码,所有的逻辑都很清晰了.这里有4个授权返回值.回到之前代码会进行如下转换:

        public virtual async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object? resource)
        {
            if (policy == null)
            {
                throw new ArgumentNullException(nameof(policy));
            }

            var result = await _authorization.AuthorizeAsync(context.User, resource, policy);
            if (result.Succeeded)
            {
                return PolicyAuthorizationResult.Success();
            }

            // If authentication was successful, return forbidden, otherwise challenge
            return (authenticationResult.Succeeded)
                ? PolicyAuthorizationResult.Forbid(result.Failure)
                : PolicyAuthorizationResult.Challenge();
        }

如果授权检验成功不存在失败直接返回PolicyAuthorizationResult.Success();授权检验但是存在失败会返回PolicyAuthorizationResult.Forbid(result.Failure),如果授权失败PolicyAuthorizationResult.Challenge();最后中间价会拿到这个返回值.到这里通过当前httpContext上下文和当前控制器方法的Authirze特性的相关信息,执行相关处理器之后,拿到了最后的授权结果,那么回到中间件,代码如下:

            var authorizationMiddlewareResultHandler = context.RequestServices.GetRequiredService<IAuthorizationMiddlewareResultHandler>();
            await authorizationMiddlewareResultHandler.HandleAsync(_next, context, policy, authorizeResult);

中间件拿到授权结果之后,调用结果处理器,执行如下代码:

        public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
        {
            if (authorizeResult.Challenged)
            {
                if (policy.AuthenticationSchemes.Count > 0)
                {
                    foreach (var scheme in policy.AuthenticationSchemes)
                    {
                        await context.ChallengeAsync(scheme);
                    }
                }
                else
                {
                    await context.ChallengeAsync();
                }

                return;
            }
            else if (authorizeResult.Forbidden)
            {
                if (policy.AuthenticationSchemes.Count > 0)
                {
                    foreach (var scheme in policy.AuthenticationSchemes)
                    {
                        await context.ForbidAsync(scheme);
                    }
                }
                else
                {
                    await context.ForbidAsync();
                }

                return;
            }

            await next(context);
        }

 显然,这里会触发Challenged,因为第一次访问,当前上下文用户显然是没有通过认证的,接着不管控制器方法有没有特别指定认证方案,这里都会进行认证方案的ChallengeAsync

流程,到这里关于授权环节的代码解析结束.

 

posted @ 2022-06-14 18:01  郑小超  阅读(334)  评论(0编辑  收藏  举报