上篇文章我介绍了如何在网关上实现客户端自定义限流功能,基本完成了关于网关的一些自定义扩展需求,后面几篇将介绍基于
IdentityServer4(后面简称Ids4)
的认证相关知识,在具体介绍ids4
实现咱们统一认证的相关功能前,咱们首先须要分析下Ids4
源码,便于咱们完全掌握认证的原理以及后续的扩展需求。html.netcore项目实战交流群(637326624),有兴趣的朋友能够在群里交流讨论。git
文档地址 http://docs.identityserver.io/en/latest/github
Github源码地址 https://github.com/IdentityServer/IdentityServer4数据库
【工欲善其事,必先利其器,器欲尽其能,必先得其法】json
在咱们使用Ids4
前咱们须要了解它的运行原理和实现方式,这样实际生产环境中才能安心使用,即便遇到问题也能够很快解决,如须要对认证进行扩展,也可自行编码实现。c#
源码分析第一步就是要找到Ids4
的中间件是如何运行的,因此须要定位到中间价应用位置app.UseIdentityServer();
,查看到详细的代码以下。api
/// <summary> /// Adds IdentityServer to the pipeline. /// </summary> /// <param name="app">The application.</param> /// <returns></returns> public static IApplicationBuilder UseIdentityServer(this IApplicationBuilder app) { //一、验证配置信息 app.Validate(); //二、应用BaseUrl中间件 app.UseMiddleware<BaseUrlMiddleware>(); //三、应用跨域访问配置 app.ConfigureCors(); //四、启用系统认证功能 app.UseAuthentication(); //五、应用ids4中间件 app.UseMiddleware<IdentityServerMiddleware>(); return app; }
经过上面的源码,咱们知道总体流程分为这5步实现。接着咱们分析下每一步都作了哪些操做呢?跨域
校验IPersistedGrantStore、IClientStore、IResourceStore
是否已经注入?服务器
验证IdentityServerOptions
配置信息是否都配置完整cookie
输出调试相关信息提醒
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; }
详细的实现代码如上因此,很是清晰明了,这时候有人确定会问这些相关的信息时从哪来的呢?这块咱们会在后面讲解。
源码以下,就是从配置信息里校验是否设置了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
实现自动的服务发现和注册,达到动态扩展认证节点功能。
可能表述的不太清楚,能够先试着理解下,由于后续篇幅有介绍负载均衡案例会讲到实际应用。
其实这个从字面意思就能够看出来,是配置跨域访问的中间件,源码就是应用配置的跨域策略。
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); } } }
很简单吧,至于什么是跨域,可自行查阅相关文档,因为篇幅有效,这里不详细解释。
就是启用了默认的认证中间件,而后在相关的控制器增长[Authorize]
属性标记便可完成认证操做,因为本篇是介绍的Ids4
的源码,因此关于非Ids4
部分后续有需求再详细介绍实现原理。
这也是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."); // 一、验证请求是否为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"); //二、判断是否开启了路由发现功能 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); // 三、生成路由相关的输出信息 var response = await _responseGenerator.CreateDiscoveryDocumentAsync(baseUrl, issuerUri); //五、返回路由发现的结果信息 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和就相对复杂一点,而后分析方式同样。
有了上面的分析,咱们能够知道整个受权的流程,全部在咱们使用Ids4
时须要注意中间件的执行顺序,针对须要受权后才能继续操做的中间件须要放到Ids4
中间件后面。
为何把这块单独列出来呢?由于后续不少扩展和应用都是基础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."); // 一、验证是否为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."); // 二、验证客户端受权是否正确 var clientResult = await _clientValidator.ValidateAsync(context); if (clientResult.Client == null) { return Error(OidcConstants.TokenErrors.InvalidClient); } /* 三、验证请求信息,详细代码(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); } // 四、建立输出结果 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)); //五、写入日志,便于调试 LogTokens(response, requestResult); // 六、返回最终的结果 _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
进行讨论。