1. 程式人生 > >【.NET Core項目實戰-統一認證平臺】第八章 授權篇-IdentityServer4源碼分析

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

pst 設置 acc 驗證過 authorize 必須 匹配 objects get

原文:【.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原始實例地址,如果設置了修改下請求的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進行討論。

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