【.NET Core项目实战-统一认证平台】第八章 授权篇-IdentityServer4源码分析
【.NET Core项目实战-统一认证平台】开篇及目录索引
上篇文章我介绍了如何在网关上实现客户端自定义限流功能,基本完成了关于网关的一些自定义扩展需求,后面几篇将介绍基于
IdentityServer4(后面简称Ids4)
的认证相关知识,在具体介绍ids4
实现我们统一认证的相关功能前,我们首先需要分析下Ids4
源码,便于我们彻底掌握认证的原理以及后续的扩展需求。.netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。
一、Ids4文档及源码
文档地址 http://docs.identityserver.io/en/latest/
Github源码地址 https://github.com/IdentityServer/IdentityServer4
二、源码整体分析
【工欲善其事,必先利其器,器欲尽其能,必先得其法】
在我们使用Ids4
前我们需要了解它的运行原理和实现方式,这样实际生产环境中才能安心使用,即使遇到问题也可以很快解决,如需要对认证进行扩展,也可自行编码实现。
源码分析第一步就是要找到Ids4
的中间件是如何运行的,所以需要定位到中间价应用位置app.UseIdentityServer();
,查看到详细的代码如下。
/// <summary>
/// Adds IdentityServer to the pipeline.
/// </summary>
/// <param name="app">The application.</param>
/// <returns></returns>
public static IApplicationBuilder UseIdentityServer(this IApplicationBuilder app)
{
//1、验证配置信息
app.Validate();
//2、应用BaseUrl中间件
app.UseMiddleware<BaseUrlMiddleware>();
//3、应用跨域访问配置
app.ConfigureCors();
//4、启用系统认证功能
app.UseAuthentication();
//5、应用ids4中间件
app.UseMiddleware<IdentityServerMiddleware>();
return app;
}
通过上面的源码,我们知道整体流程分为这5步实现。接着我们分析下每一步都做了哪些操作呢?
1、app.Validate()为我们做了哪些工作?
-
校验
IPersistedGrantStore、IClientStore、IResourceStore
是否已经注入? -
验证
IdentityServerOptions
配置信息是否都配置完整 -
输出调试相关信息提醒
internal static void Validate(this IApplicationBuilder app) { var loggerFactory = app.ApplicationServices.GetService(typeof(ILoggerFactory)) as ILoggerFactory; if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); var logger = loggerFactory.CreateLogger("IdentityServer4.Startup"); var scopeFactory = app.ApplicationServices.GetService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { var serviceProvider = scope.ServiceProvider; TestService(serviceProvider, typeof(IPersistedGrantStore), logger, "No storage mechanism for grants specified. Use the 'AddInMemoryPersistedGrants' extension method to register a development version."); TestService(serviceProvider, typeof(IClientStore), logger, "No storage mechanism for clients specified. Use the 'AddInMemoryClients' extension method to register a development version."); TestService(serviceProvider, typeof(IResourceStore), logger, "No storage mechanism for resources specified. Use the 'AddInMemoryIdentityResources' or 'AddInMemoryApiResources' extension method to register a development version."); var persistedGrants = serviceProvider.GetService(typeof(IPersistedGrantStore)); if (persistedGrants.GetType().FullName == typeof(InMemoryPersistedGrantStore).FullName) { logger.LogInformation("You are using the in-memory version of the persisted grant store. This will store consent decisions, authorization codes, refresh and reference tokens in memory only. If you are using any of those features in production, you want to switch to a different store implementation."); } var options = serviceProvider.GetRequiredService<IdentityServerOptions>(); ValidateOptions(options, logger); ValidateAsync(serviceProvider, logger).GetAwaiter().GetResult(); } } private static async Task ValidateAsync(IServiceProvider services, ILogger logger) { var options = services.GetRequiredService<IdentityServerOptions>(); var schemes = services.GetRequiredService<IAuthenticationSchemeProvider>(); if (await schemes.GetDefaultAuthenticateSchemeAsync() == null && options.Authentication.CookieAuthenticationScheme == null) { logger.LogWarning("No authentication scheme has been set. Setting either a default authentication scheme or a CookieAuthenticationScheme on IdentityServerOptions is required."); } else { if (options.Authentication.CookieAuthenticationScheme != null) { logger.LogInformation("Using explicitly configured scheme {scheme} for IdentityServer", options.Authentication.CookieAuthenticationScheme); } logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for authentication", (await schemes.GetDefaultAuthenticateSchemeAsync())?.Name); logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for sign-in", (await schemes.GetDefaultSignInSchemeAsync())?.Name); logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for sign-out", (await schemes.GetDefaultSignOutSchemeAsync())?.Name); logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for challenge", (await schemes.GetDefaultChallengeSchemeAsync())?.Name); logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for forbid", (await schemes.GetDefaultForbidSchemeAsync())?.Name); } } private static void ValidateOptions(IdentityServerOptions options, ILogger logger) { if (options.IssuerUri.IsPresent()) logger.LogDebug("Custom IssuerUri set to {0}", options.IssuerUri); if (options.PublicOrigin.IsPresent()) { if (!Uri.TryCreate(options.PublicOrigin, UriKind.Absolute, out var uri)) { throw new InvalidOperationException($"PublicOrigin is not valid: {options.PublicOrigin}"); } logger.LogDebug("PublicOrigin explicitly set to {0}", options.PublicOrigin); } // todo: perhaps different logging messages? //if (options.UserInteraction.LoginUrl.IsMissing()) throw new InvalidOperationException("LoginUrl is not configured"); //if (options.UserInteraction.LoginReturnUrlParameter.IsMissing()) throw new InvalidOperationException("LoginReturnUrlParameter is not configured"); //if (options.UserInteraction.LogoutUrl.IsMissing()) throw new InvalidOperationException("LogoutUrl is not configured"); if (options.UserInteraction.LogoutIdParameter.IsMissing()) throw new InvalidOperationException("LogoutIdParameter is not configured"); if (options.UserInteraction.ErrorUrl.IsMissing()) throw new InvalidOperationException("ErrorUrl is not configured"); if (options.UserInteraction.ErrorIdParameter.IsMissing()) throw new InvalidOperationException("ErrorIdParameter is not configured"); if (options.UserInteraction.ConsentUrl.IsMissing()) throw new InvalidOperationException("ConsentUrl is not configured"); if (options.UserInteraction.ConsentReturnUrlParameter.IsMissing()) throw new InvalidOperationException("ConsentReturnUrlParameter is not configured"); if (options.UserInteraction.CustomRedirectReturnUrlParameter.IsMissing()) throw new InvalidOperationException("CustomRedirectReturnUrlParameter is not configured"); if (options.Authentication.CheckSessionCookieName.IsMissing()) throw new InvalidOperationException("CheckSessionCookieName is not configured"); if (options.Cors.CorsPolicyName.IsMissing()) throw new InvalidOperationException("CorsPolicyName is not configured"); } internal static object TestService(IServiceProvider serviceProvider, Type service, ILogger logger, string message = null, bool doThrow = true) { var appService = serviceProvider.GetService(service); if (appService == null) { var error = message ?? $"Required service {service.FullName} is not registered in the DI container. Aborting startup"; logger.LogCritical(error); if (doThrow) { throw new InvalidOperationException(error); } } return appService; }
详细的实现代码如上所以,非常清晰明了,这时候有人肯定会问这些相关的信息时从哪来的呢?这块我们会在后面讲解。
2、BaseUrlMiddleware中间件实现了什么功能?
源码如下,就是从配置信息里校验是否设置了PublicOrigin
原始实例地址,如果设置了修改下请求的Scheme
和Host
,最后设置IdentityServerBasePath
地址信息,然后把请求转到下一个路由。
namespace IdentityServer4.Hosting
{
public class BaseUrlMiddleware
{
private readonly RequestDelegate _next;
private readonly IdentityServerOptions _options;
public BaseUrlMiddleware(RequestDelegate next, IdentityServerOptions options)
{
_next = next;
_options = options;
}
public async Task Invoke(HttpContext context)
{
var request = context.Request;
if (_options.PublicOrigin.IsPresent())
{
context.SetIdentityServerOrigin(_options.PublicOrigin);
}
context.SetIdentityServerBasePath(request.PathBase.Value.RemoveTrailingSlash());
await _next(context);
}
}
}
这里源码非常简单,就是设置了后期要处理的一些关于请求地址信息。那这个中间件有什么作用呢?
就是设置认证的通用地址,当我们访问认证服务配置地址http://localhost:5000/.well-known/openid-configuration
的时候您会发现,您设置的PublicOrigin
会自定应用到所有的配置信息前缀,比如设置option.PublicOrigin = "http://www.baidu.com";
,显示的json
代码如下。
{"issuer":"http://www.baidu.com","jwks_uri":"http://www.baidu.com/.well-known/openid-configuration/jwks","authorization_endpoint":"http://www.baidu.com/connect/authorize","token_endpoint":"http://www.baidu.com/connect/token","userinfo_endpoint":"http://www.baidu.com/connect/userinfo","end_session_endpoint":"http://www.baidu.com/connect/endsession","check_session_iframe":"http://www.baidu.com/connect/checksession","revocation_endpoint":"http://www.baidu.com/connect/revocation","introspection_endpoint":"http://www.baidu.com/connect/introspect","frontchannel_logout_supported":true,"frontchannel_logout_session_supported":true,"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"scopes_supported":["api1","offline_access"],"claims_supported":[],"grant_types_supported":["authorization_code","client_credentials","refresh_token","implicit"],"response_types_supported":["code","token","id_token","id_token token","code id_token","code token","code id_token token"],"response_modes_supported":["form_post","query","fragment"],"token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"code_challenge_methods_supported":["plain","S256"]}
可能还有些朋友觉得奇怪,这有什么用啊?其实不然,试想下如果您部署的认证服务器是由多台组成,那么可以设置这个地址为负载均衡地址,这样访问每台认证服务器的配置信息,返回的负载均衡的地址,而负载均衡真正路由到的地址是内网地址,每一个实例内网地址都不一样,这样就可以负载生效,后续的文章会介绍配合Consul
实现自动的服务发现和注册,达到动态扩展认证节点功能。
可能表述的不太清楚,可以先试着理解下,因为后续篇幅有介绍负载均衡案例会讲到实际应用。
3、app.ConfigureCors(); 做了什么操作?
其实这个从字面意思就可以看出来,是配置跨域访问的中间件,源码就是应用配置的跨域策略。
namespace IdentityServer4.Hosting
{
public static class CorsMiddlewareExtensions
{
public static void ConfigureCors(this IApplicationBuilder app)
{
var options = app.ApplicationServices.GetRequiredService<IdentityServerOptions>();
app.UseCors(options.Cors.CorsPolicyName);
}
}
}
很简单吧,至于什么是跨域,可自行查阅相关文档,由于篇幅有效,这里不详细解释。
4、app.UseAuthentication();做了什么操作?
就是启用了默认的认证中间件,然后在相关的控制器增加[Authorize]
属性标记即可完成认证操作,由于本篇是介绍的Ids4
的源码,所以关于非Ids4
部分后续有需求再详细介绍实现原理。
5、IdentityServerMiddleware中间件做了什么操作?
这也是Ids4
的核心中间件,通过源码分析,哎呀!好简单啊,我要一口气写100个牛逼中间件。
哈哈,我当时也是这么想的,难道真的这么简单吗?接着往下分析,让我们彻底明白Ids4
是怎么运行的。
namespace IdentityServer4.Hosting
{
/// <summary>
/// IdentityServer middleware
/// </summary>
public class IdentityServerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="IdentityServerMiddleware"/> class.
/// </summary>
/// <param name="next">The next.</param>
/// <param name="logger">The logger.</param>
public IdentityServerMiddleware(RequestDelegate next, ILogger<IdentityServerMiddleware> logger)
{
_next = next;
_logger = logger;
}
/// <summary>
/// Invokes the middleware.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="router">The router.</param>
/// <param name="session">The user session.</param>
/// <param name="events">The event service.</param>
/// <returns></returns>
public async Task Invoke(HttpContext context, IEndpointRouter router, IUserSession session, IEventService events)
{
// this will check the authentication session and from it emit the check session
// cookie needed from JS-based signout clients.
await session.EnsureSessionIdCookieAsync();
try
{
var endpoint = router.Find(context);
if (endpoint != null)
{
_logger.LogInformation("Invoking IdentityServer endpoint: {endpointType} for {url}", endpoint.GetType().FullName, context.Request.Path.ToString());
var result = await endpoint.ProcessAsync(context);
if (result != null)
{
_logger.LogTrace("Invoking result: {type}", result.GetType().FullName);
await result.ExecuteAsync(context);
}
return;
}
}
catch (Exception ex)
{
await events.RaiseAsync(new UnhandledExceptionEvent(ex));
_logger.LogCritical(ex, "Unhandled exception: {exception}", ex.Message);
throw;
}
await _next(context);
}
}
}
第一步从本地提取授权记录,就是如果之前授权过,直接提取授权到请求上下文。说起来是一句话,但是实现起来还是比较多步骤的,我简单描述下整个流程如下。
-
执行授权
如果发现本地未授权时,获取对应的授权处理器,然后执行授权,看是否授权成功,如果授权成功,赋值相关的信息,常见的应用就是自动登录的实现。
比如用户U访问A系统信息,自动跳转到S认证系统进行认证,认证后调回A系统正常访问,这时候如果用户U访问B系统(B系统也是S统一认证的),B系统会自动跳转到S认证系统进行认证,比如跳转到
/login
页面,这时候通过检测发现用户U已经经过认证,可以直接提取认证的所有信息,然后跳转到系统B,实现了自动登录过程。private async Task AuthenticateAsync() { if (Principal == null || Properties == null) { var scheme = await GetCookieSchemeAsync(); //根据请求上下人和认证方案获取授权处理器 var handler = await Handlers.GetHandlerAsync(HttpContext, scheme); if (handler == null) { throw new InvalidOperationException($"No authentication handler is configured to authenticate for the scheme: {scheme}"); } //执行对应的授权操作 var result = await handler.AuthenticateAsync(); if (result != null && result.Succeeded) { Principal = result.Principal; Properties = result.Properties; } } }
-
获取路由处理器
其实这个功能就是拦截请求,获取对应的请求的处理器,那它是如何实现的呢?
IEndpointRouter
是这个接口专门负责处理的,那这个方法的实现方式是什么呢?可以右键-转到实现
,我们可以找到EndpointRouter
方法,详细代码如下。namespace IdentityServer4.Hosting { internal class EndpointRouter : IEndpointRouter { private readonly IEnumerable<Endpoint> _endpoints; private readonly IdentityServerOptions _options; private readonly ILogger _logger; public EndpointRouter(IEnumerable<Endpoint> endpoints, IdentityServerOptions options, ILogger<EndpointRouter> logger) { _endpoints = endpoints; _options = options; _logger = logger; } public IEndpointHandler Find(HttpContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); //遍历所有的路由和请求处理器,如果匹配上,返回对应的处理器,否则返回null foreach(var endpoint in _endpoints) { var path = endpoint.Path; if (context.Request.Path.Equals(path, StringComparison.OrdinalIgnoreCase)) { var endpointName = endpoint.Name; _logger.LogDebug("Request path {path} matched to endpoint type {endpoint}", context.Request.Path, endpointName); return GetEndpointHandler(endpoint, context); } } _logger.LogTrace("No endpoint entry found for request path: {path}", context.Request.Path); return null; } //根据判断配置文件是否开启了路由拦截功能,如果存在提取对应的处理器。 private IEndpointHandler GetEndpointHandler(Endpoint endpoint, HttpContext context) { if (_options.Endpoints.IsEndpointEnabled(endpoint)) { var handler = context.RequestServices.GetService(endpoint.Handler) as IEndpointHandler; if (handler != null) { _logger.LogDebug("Endpoint enabled: {endpoint}, successfully created handler: {endpointHandler}", endpoint.Name, endpoint.Handler.FullName); return handler; } else { _logger.LogDebug("Endpoint enabled: {endpoint}, failed to create handler: {endpointHandler}", endpoint.Name, endpoint.Handler.FullName); } } else { _logger.LogWarning("Endpoint disabled: {endpoint}", endpoint.Name); } return null; } } }
源码功能我做了简单的讲解,发现就是提取对应路由处理器,然后转换成
IEndpointHandler
接口,所有的处理器都会实现这个接口。但是IEnumerable<Endpoint>
记录是从哪里来的呢?而且为什么可以获取到指定的处理器,可以查看如下代码,原来都注入到默认的路由处理方法里。/// <summary> /// Adds the default endpoints. /// </summary> /// <param name="builder">The builder.</param> /// <returns></returns> public static IIdentityServerBuilder AddDefaultEndpoints(this IIdentityServerBuilder builder) { builder.Services.AddTransient<IEndpointRouter, EndpointRouter>(); builder.AddEndpoint<AuthorizeCallbackEndpoint>(EndpointNames.Authorize, ProtocolRoutePaths.AuthorizeCallback.EnsureLeadingSlash()); builder.AddEndpoint<AuthorizeEndpoint>(EndpointNames.Authorize, ProtocolRoutePaths.Authorize.EnsureLeadingSlash()); builder.AddEndpoint<CheckSessionEndpoint>(EndpointNames.CheckSession, ProtocolRoutePaths.CheckSession.EnsureLeadingSlash()); builder.AddEndpoint<DiscoveryKeyEndpoint>(EndpointNames.Discovery, ProtocolRoutePaths.DiscoveryWebKeys.EnsureLeadingSlash()); builder.AddEndpoint<DiscoveryEndpoint>(EndpointNames.Discovery, ProtocolRoutePaths.DiscoveryConfiguration.EnsureLeadingSlash()); builder.AddEndpoint<EndSessionCallbackEndpoint>(EndpointNames.EndSession, ProtocolRoutePaths.EndSessionCallback.EnsureLeadingSlash()); builder.AddEndpoint<EndSessionEndpoint>(EndpointNames.EndSession, ProtocolRoutePaths.EndSession.EnsureLeadingSlash()); builder.AddEndpoint<IntrospectionEndpoint>(EndpointNames.Introspection, ProtocolRoutePaths.Introspection.EnsureLeadingSlash()); builder.AddEndpoint<TokenRevocationEndpoint>(EndpointNames.Revocation, ProtocolRoutePaths.Revocation.EnsureLeadingSlash()); builder.AddEndpoint<TokenEndpoint>(EndpointNames.Token, ProtocolRoutePaths.Token.EnsureLeadingSlash()); builder.AddEndpoint<UserInfoEndpoint>(EndpointNames.UserInfo, ProtocolRoutePaths.UserInfo.EnsureLeadingSlash()); return builder; } /// <summary> /// Adds the endpoint. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="builder">The builder.</param> /// <param name="name">The name.</param> /// <param name="path">The path.</param> /// <returns></returns> public static IIdentityServerBuilder AddEndpoint<T>(this IIdentityServerBuilder builder, string name, PathString path) where T : class, IEndpointHandler { builder.Services.AddTransient<T>(); builder.Services.AddSingleton(new Endpoint(name, path, typeof(T))); return builder; }
通过现在分析,我们知道了路由查找方法的原理了,以后我们想增加自定义的拦截器也知道从哪里下手了。
-
-
执行路由过程并返回结果
有了这些基础知识后,就可以很好的理解
var result = await endpoint.ProcessAsync(context);
这句话了,其实业务逻辑还是在自己的处理器里,但是可以通过调用接口方法实现,是不是非常优雅呢?为了更进一步理解,我们就上面列出的路由发现地址(
http://localhost:5000/.well-known/openid-configuration
)为例,讲解下运行过程。通过注入方法可以发现,路由发现的处理器如下所示。
builder.AddEndpoint<DiscoveryEndpoint>(EndpointNames.Discovery, ProtocolRoutePaths.DiscoveryConfiguration.EnsureLeadingSlash());
//协议默认路由地址
public static class ProtocolRoutePaths
{
public const string Authorize = "connect/authorize";
public const string AuthorizeCallback = Authorize + "/callback";
public const string DiscoveryConfiguration = ".well-known/openid-configuration";
public const string DiscoveryWebKeys = DiscoveryConfiguration + "/jwks";
public const string Token = "connect/token";
public const string Revocation = "connect/revocation";
public const string UserInfo = "connect/userinfo";
public const string Introspection = "connect/introspect";
public const string EndSession = "connect/endsession";
public const string EndSessionCallback = EndSession + "/callback";
public const string CheckSession = "connect/checksession";
public static readonly string[] CorsPaths =
{
DiscoveryConfiguration,
DiscoveryWebKeys,
Token,
UserInfo,
Revocation
};
}
可以请求的地址会被拦截,然后进行处理。
它的详细代码如下,跟分析的一样是实现了IEndpointHandler
接口。
using System.Net;
using System.Threading.Tasks;
using IdentityServer4.Configuration;
using IdentityServer4.Endpoints.Results;
using IdentityServer4.Extensions;
using IdentityServer4.Hosting;
using IdentityServer4.ResponseHandling;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace IdentityServer4.Endpoints
{
internal class DiscoveryEndpoint : IEndpointHandler
{
private readonly ILogger _logger;
private readonly IdentityServerOptions _options;
private readonly IDiscoveryResponseGenerator _responseGenerator;
public DiscoveryEndpoint(
IdentityServerOptions options,
IDiscoveryResponseGenerator responseGenerator,
ILogger<DiscoveryEndpoint> logger)
{
_logger = logger;
_options = options;
_responseGenerator = responseGenerator;
}
public async Task<IEndpointResult> ProcessAsync(HttpContext context)
{
_logger.LogTrace("Processing discovery request.");
// 1、验证请求是否为Get方法
if (!HttpMethods.IsGet(context.Request.Method))
{
_logger.LogWarning("Discovery endpoint only supports GET requests");
return new StatusCodeResult(HttpStatusCode.MethodNotAllowed);
}
_logger.LogDebug("Start discovery request");
//2、判断是否开启了路由发现功能
if (!_options.Endpoints.EnableDiscoveryEndpoint)
{
_logger.LogInformation("Discovery endpoint disabled. 404.");
return new StatusCodeResult(HttpStatusCode.NotFound);
}
var baseUrl = context.GetIdentityServerBaseUrl().EnsureTrailingSlash();
var issuerUri = context.GetIdentityServerIssuerUri();
_logger.LogTrace("Calling into discovery response generator: {type}", _responseGenerator.GetType().FullName);
// 3、生成路由相关的输出信息
var response = await _responseGenerator.CreateDiscoveryDocumentAsync(baseUrl, issuerUri);
//5、返回路由发现的结果信息
return new DiscoveryDocumentResult(response, _options.Discovery.ResponseCacheInterval);
}
}
}
通过上面代码说明,可以发现通过4步完成了整个解析过程,然后输出最终结果,终止管道继续往下进行。
if (result != null)
{
_logger.LogTrace("Invoking result: {type}", result.GetType().FullName);
await result.ExecuteAsync(context);
}
return;
路由发现的具体实现代码如下,就是把结果转换成Json格式输出,然后就得到了我们想要的结果。
/// <summary>
/// Executes the result.
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <returns></returns>
public Task ExecuteAsync(HttpContext context)
{
if (MaxAge.HasValue && MaxAge.Value >= 0)
{
context.Response.SetCache(MaxAge.Value);
}
return context.Response.WriteJsonAsync(ObjectSerializer.ToJObject(Entries));
}
到此完整的路由发现功能及实现了,其实这个实现比较简单,因为没有涉及太多其他关联的东西,像获取Token和就相对复杂一点,然后分析方式一样。
6、继续运行下一个中间件
有了上面的分析,我们可以知道整个授权的流程,所有在我们使用Ids4
时需要注意中间件的执行顺序,针对需要授权后才能继续操作的中间件需要放到Ids4
中间件后面。
三、获取Token执行分析
为什么把这块单独列出来呢?因为后续很多扩展和应用都是基础Token获取的流程,所以有必要单独把这块拿出来进行讲解。有了前面整体的分析,现在应该直接这块源码是从哪里看了,没错就是下面这句。
builder.AddEndpoint<TokenEndpoint>(EndpointNames.Token, ProtocolRoutePaths.Token.EnsureLeadingSlash());
他的执行过程是TokenEndpoint
,所以我们重点来分析下这个是怎么实现这么复杂的获取Token过程的,首先放源码。
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using IdentityModel;
using IdentityServer4.Endpoints.Results;
using IdentityServer4.Events;
using IdentityServer4.Extensions;
using IdentityServer4.Hosting;
using IdentityServer4.ResponseHandling;
using IdentityServer4.Services;
using IdentityServer4.Validation;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace IdentityServer4.Endpoints
{
/// <summary>
/// The token endpoint
/// </summary>
/// <seealso cref="IdentityServer4.Hosting.IEndpointHandler" />
internal class TokenEndpoint : IEndpointHandler
{
private readonly IClientSecretValidator _clientValidator;
private readonly ITokenRequestValidator _requestValidator;
private readonly ITokenResponseGenerator _responseGenerator;
private readonly IEventService _events;
private readonly ILogger _logger;
/// <summary>
/// 构造函数注入 <see cref="TokenEndpoint" /> class.
/// </summary>
/// <param name="clientValidator">客户端验证处理器</param>
/// <param name="requestValidator">请求验证处理器</param>
/// <param name="responseGenerator">输出生成处理器</param>
/// <param name="events">事件处理器.</param>
/// <param name="logger">日志</param>
public TokenEndpoint(
IClientSecretValidator clientValidator,
ITokenRequestValidator requestValidator,
ITokenResponseGenerator responseGenerator,
IEventService events,
ILogger<TokenEndpoint> logger)
{
_clientValidator = clientValidator;
_requestValidator = requestValidator;
_responseGenerator = responseGenerator;
_events = events;
_logger = logger;
}
/// <summary>
/// Processes the request.
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <returns></returns>
public async Task<IEndpointResult> ProcessAsync(HttpContext context)
{
_logger.LogTrace("Processing token request.");
// 1、验证是否为Post请求且必须是form-data方式
if (!HttpMethods.IsPost(context.Request.Method) || !context.Request.HasFormContentType)
{
_logger.LogWarning("Invalid HTTP request for token endpoint");
return Error(OidcConstants.TokenErrors.InvalidRequest);
}
return await ProcessTokenRequestAsync(context);
}
private async Task<IEndpointResult> ProcessTokenRequestAsync(HttpContext context)
{
_logger.LogDebug("Start token request.");
// 2、验证客户端授权是否正确
var clientResult = await _clientValidator.ValidateAsync(context);
if (clientResult.Client == null)
{
return Error(OidcConstants.TokenErrors.InvalidClient);
}
/* 3、验证请求信息,详细代码(TokenRequestValidator.cs)
原理就是根据不同的Grant_Type,调用不同的验证方式
*/
var form = (await context.Request.ReadFormAsync()).AsNameValueCollection();
_logger.LogTrace("Calling into token request validator: {type}", _requestValidator.GetType().FullName);
var requestResult = await _requestValidator.ValidateRequestAsync(form, clientResult);
if (requestResult.IsError)
{
await _events.RaiseAsync(new TokenIssuedFailureEvent(requestResult));
return Error(requestResult.Error, requestResult.ErrorDescription, requestResult.CustomResponse);
}
// 4、创建输出结果 TokenResponseGenerator.cs
_logger.LogTrace("Calling into token request response generator: {type}", _responseGenerator.GetType().FullName);
var response = await _responseGenerator.ProcessAsync(requestResult);
//发送token生成事件
await _events.RaiseAsync(new TokenIssuedSuccessEvent(response, requestResult));
//5、写入日志,便于调试
LogTokens(response, requestResult);
// 6、返回最终的结果
_logger.LogDebug("Token request success.");
return new TokenResult(response);
}
private TokenErrorResult Error(string error, string errorDescription = null, Dictionary<string, object> custom = null)
{
var response = new TokenErrorResponse
{
Error = error,
ErrorDescription = errorDescription,
Custom = custom
};
return new TokenErrorResult(response);
}
private void LogTokens(TokenResponse response, TokenRequestValidationResult requestResult)
{
var clientId = $"{requestResult.ValidatedRequest.Client.ClientId} ({requestResult.ValidatedRequest.Client?.ClientName ?? "no name set"})";
var subjectId = requestResult.ValidatedRequest.Subject?.GetSubjectId() ?? "no subject";
if (response.IdentityToken != null)
{
_logger.LogTrace("Identity token issued for {clientId} / {subjectId}: {token}", clientId, subjectId, response.IdentityToken);
}
if (response.RefreshToken != null)
{
_logger.LogTrace("Refresh token issued for {clientId} / {subjectId}: {token}", clientId, subjectId, response.RefreshToken);
}
if (response.AccessToken != null)
{
_logger.LogTrace("Access token issued for {clientId} / {subjectId}: {token}", clientId, subjectId, response.AccessToken);
}
}
}
}
执行步骤如下:
-
验证是否为Post请求且使用form-data方式传递参数(直接看代码即可)
-
验证客户端授权
详细的验证流程代码和说明如下。
ClientSecretValidator.cs
public async Task<ClientSecretValidationResult> ValidateAsync(HttpContext context) { _logger.LogDebug("Start client validation"); var fail = new ClientSecretValidationResult { IsError = true }; // 从上下文中判断是否存在 client_id 和 client_secret信息(PostBodySecretParser.cs) var parsedSecret = await _parser.ParseAsync(context); if (parsedSecret == null) { await RaiseFailureEventAsync("unknown", "No client id found"); _logger.LogError("No client identifier found"); return fail; } // 通过client_id从客户端获取(IClientStore,客户端接口,下篇会介绍如何重写) var client = await _clients.FindEnabledClientByIdAsync(parsedSecret.Id); if (client == null) {//不存在直接输出错误 await RaiseFailureEventAsync(parsedSecret.Id, "Unknown client"); _logger.LogError("No client with id '{clientId}' found. aborting", parsedSecret.Id); return fail; } SecretValidationResult secretValidationResult = null; if (!client.RequireClientSecret || client.IsImplicitOnly()) {//判断客户端是否启用验证或者匿名访问,不进行密钥验证 _logger.LogDebug("Public Client - skipping secret validation success"); } else { //验证密钥是否一致 secretValidationResult = await _validator.ValidateAsync(parsedSecret, client.ClientSecrets); if (secretValidationResult.Success == false) { await RaiseFailureEventAsync(client.ClientId, "Invalid client secret"); _logger.LogError("Client secret validation failed for client: {clientId}.", client.ClientId); return fail; } } _logger.LogDebug("Client validation success"); var success = new ClientSecretValidationResult { IsError = false, Client = client, Secret = parsedSecret, Confirmation = secretValidationResult?.Confirmation }; //发送验证成功事件 await RaiseSuccessEventAsync(client.ClientId, parsedSecret.Type); return success; }
PostBodySecretParser.cs
/// <summary> /// Tries to find a secret on the context that can be used for authentication /// </summary> /// <param name="context">The HTTP context.</param> /// <returns> /// A parsed secret /// </returns> public async Task<ParsedSecret> ParseAsync(HttpContext context) { _logger.LogDebug("Start parsing for secret in post body"); if (!context.Request.HasFormContentType) { _logger.LogDebug("Content type is not a form"); return null; } var body = await context.Request.ReadFormAsync(); if (body != null) { var id = body["client_id"].FirstOrDefault(); var secret = body["client_secret"].FirstOrDefault(); // client id must be present if (id.IsPresent()) { if (id.Length > _options.InputLengthRestrictions.ClientId) { _logger.LogError("Client ID exceeds maximum length."); return null; } if (secret.IsPresent()) { if (secret.Length > _options.InputLengthRestrictions.ClientSecret) { _logger.LogError("Client secret exceeds maximum length."); return null; } return new ParsedSecret { Id = id, Credential = secret, Type = IdentityServerConstants.ParsedSecretTypes.SharedSecret }; } else { // client secret is optional _logger.LogDebug("client id without secret found"); return new ParsedSecret { Id = id, Type = IdentityServerConstants.ParsedSecretTypes.NoSecret }; } } } _logger.LogDebug("No secret in post body found"); return null; }
-
验证请求的信息是否有误
由于代码太多,只列出
TokenRequestValidator.cs
部分核心代码如下,
-
//是不是很熟悉,不同的授权方式
switch (grantType)
{
case OidcConstants.GrantTypes.AuthorizationCode: //授权码模式
return await RunValidationAsync(ValidateAuthorizationCodeRequestAsync, parameters);
case OidcConstants.GrantTypes.ClientCredentials: //客户端模式
return await RunValidationAsync(ValidateClientCredentialsRequestAsync, parameters);
case OidcConstants.GrantTypes.Password: //密码模式
return await RunValidationAsync(ValidateResourceOwnerCredentialRequestAsync, parameters);
case OidcConstants.GrantTypes.RefreshToken: //token更新
return await RunValidationAsync(ValidateRefreshTokenRequestAsync, parameters);
default:
return await RunValidationAsync(ValidateExtensionGrantRequestAsync, parameters); //扩展模式,后面的篇章会介绍扩展方式
}
- 创建生成的结果
TokenResponseGenerator.cs
根据不同的认证方式执行不同的创建方法,由于篇幅有限,每一个是如何创建的可以自行查看源码。
/// <summary>
/// Processes the response.
/// </summary>
/// <param name="request">The request.</param>
/// <returns></returns>
public virtual async Task<TokenResponse> ProcessAsync(TokenRequestValidationResult request)
{
switch (request.ValidatedRequest.GrantType)
{
case OidcConstants.GrantTypes.ClientCredentials:
return await ProcessClientCredentialsRequestAsync(request);
case OidcConstants.GrantTypes.Password:
return await ProcessPasswordRequestAsync(request);
case OidcConstants.GrantTypes.AuthorizationCode:
return await ProcessAuthorizationCodeRequestAsync(request);
case OidcConstants.GrantTypes.RefreshToken:
return await ProcessRefreshTokenRequestAsync(request);
default:
return await ProcessExtensionGrantRequestAsync(request);
}
}
-
写入日志记录
为了调试方便,把生成的token相关结果写入到日志里。
-
输出最终结果
把整个执行后的结果进行输出,这样就完成了整个验证过程。
四、总结
通过前面的分析,我们基本掌握的Ids4
整体的运行流程和具体一个认证请求的流程,由于源码太多,就未展开详细的分析每一步的实现,具体的实现细节我会在后续Ids4
相关章节中针对每一项的实现进行讲解,本篇基本都是全局性的东西,也在讲解了了解到了客户端的认证方式,但是只是介绍了接口,至于接口如何实现没有讲解,下一篇我们将介绍Ids4
实现自定义的存储并使用dapper
替换EFCore
实现与数据库的交互流程,减少不必要的请求开销。
对于本篇源码解析还有不理解的,可以进入QQ群:637326624
进行讨论。