原文: 【.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認證系統進行認證,比如跳轉到
頁面,這時候通過檢測發現使用者U已經經過認證,可以直接提取認證的所有資訊,然後跳轉到系統B,實作了自動登入過程。/login
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
進行讨論。