【.NET Core項目實戰-統一認證平臺】第八章 受權篇-IdentityServer4源碼分析

【.NET Core項目實戰-統一認證平臺】開篇及目錄索引

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

.netcore項目實戰交流羣(637326624),有興趣的朋友能夠在羣裏交流討論。git

1、Ids4文檔及源碼

文檔地址 http://docs.identityserver.io/en/latest/github

Github源碼地址 https://github.com/IdentityServer/IdentityServer4數據庫

2、源碼總體分析

【工欲善其事,必先利其器,器欲盡其能,必先得其法】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步實現。接着咱們分析下每一步都作了哪些操做呢?跨域

一、app.Validate()爲咱們作了哪些工做?

  • 校驗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;
    }

    詳細的實現代碼如上因此,很是清晰明瞭,這時候有人確定會問這些相關的信息時從哪來的呢?這塊咱們會在後面講解。

    二、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實現自動的服務發現和註冊,達到動態擴展認證節點功能。

可能表述的不太清楚,能夠先試着理解下,由於後續篇幅有介紹負載均衡案例會講到實際應用。

三、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);
        }
    }
}

很簡單吧,至於什麼是跨域,可自行查閱相關文檔,因爲篇幅有效,這裏不詳細解釋。

四、app.UseAuthentication();作了什麼操做?

就是啓用了默認的認證中間件,而後在相關的控制器增長[Authorize]屬性標記便可完成認證操做,因爲本篇是介紹的Ids4的源碼,因此關於非Ids4部分後續有需求再詳細介紹實現原理。

五、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.");
   
               // 一、驗證請求是否爲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中間件後面。

3、獲取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.");

            // 一、驗證是否爲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);
            }
        }
    }
}

執行步驟以下:

  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. 輸出最終結果

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

4、總結

經過前面的分析,咱們基本掌握的Ids4總體的運行流程和具體一個認證請求的流程,因爲源碼太多,就未展開詳細的分析每一步的實現,具體的實現細節我會在後續Ids4相關章節中針對每一項的實現進行講解,本篇基本都是全局性的東西,也在講解了瞭解到了客戶端的認證方式,可是隻是介紹了接口,至於接口如何實現沒有講解,下一篇咱們將介紹Ids4實現自定義的存儲並使用dapper替換EFCore實現與數據庫的交互流程,減小沒必要要的請求開銷。

對於本篇源碼解析還有不理解的,能夠進入QQ羣:637326624進行討論。

相關文章
相關標籤/搜索