.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
流程,到这里关于授权环节的代码解析结束.