1. 程式人生 > >【.NET Core專案實戰-統一認證平臺】第八章 認證篇-IdentityServer4原始碼分析

【.NET Core專案實戰-統一認證平臺】第八章 認證篇-IdentityServer4原始碼分析

上篇文章我介紹瞭如何在閘道器上實現客戶端自定義限流功能,基本完成了關於閘道器的一些自定義擴充套件需求,後面幾篇將介紹基於IdentityServer4(後面簡稱Ids4)的認證相關知識,在具體介紹ids4實現我們統一認證的相關功能前,我們首先需要分析下Ids4原始碼,便於我們徹底掌握認證的原理以及後續的擴充套件需求。

.netcore專案實戰交流群(637326624),有興趣的朋友可以在群裡交流討論。

一、Ids4文件及原始碼

二、原始碼整體分析

【工欲善其事,必先利其器,器欲盡其能,必先得其法】

在我們使用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原始例項地址,如果設定了修改下請求的SchemeHost,最後設定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);
        }
    }
}

第一步從本地提取授權記錄,就是如果之前授權過,直接提取授權到請求上下文。說起來是一句話,但是實現起來還是比較多步驟的,我簡單描述下整個流程如下。

  1. 執行授權

    如果發現本地未授權時,獲取對應的授權處理器,然後執行授權,看是否授權成功,如果授權成功,賦值相關的資訊,常見的應用就是自動登入的實現。

    比如使用者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;
            }
        }
    }
    1. 獲取路由處理器

      其實這個功能就是攔截請求,獲取對應的請求的處理器,那它是如何實現的呢?

      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;
              }

      通過現在分析,我們知道了路由查詢方法的原理了,以後我們想增加自定義的攔截器也知道從哪裡下手了。

  2. 執行路由過程並返回結果

    有了這些基礎知識後,就可以很好的理解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);
            }
        }
    }
}

執行步驟如下:

  1. 驗證是否為Post請求且使用form-data方式傳遞引數(直接看程式碼即可)

  2. 驗證客戶端授權

    詳細的驗證流程程式碼和說明如下。

    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;
    }
    1. 驗證請求的資訊是否有誤

      由於程式碼太多,只列出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);  //擴充套件模式,後面的篇章會介紹擴充套件方式
}
  1. 建立生成的結果

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);
    }
}
  1. 寫入日誌記錄

    為了除錯方便,把生成的token相關結果寫入到日誌裡。

  2. 輸出最終結果

    把整個執行後的結果進行輸出,這樣就完成了整個驗證過程。

四、總結

通過前面的分析,我們基本掌握的Ids4整體的執行流程和具體一個認證請求的流程,由於原始碼太多,就未展開詳細的分析每一步的實現,具體的實現細節我會在後續Ids4相關章節中針對每一項的實現進行講解,本篇基本都是全域性性的東西,也在講解了瞭解到了客戶端的認證方式,但是隻是介紹了介面,至於介面如何實現沒有講解,下一篇我們將介紹Ids4實現自定義的儲存並使用dapper替換EFCore實現與資料庫的互動流程,減少不必要的請求開銷。

對於本篇原始碼解析還有不理解的,可以進入QQ群:637326624進行討論。