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

来源:https://www.cnblogs.com/jackcao/archive/2018/11/28/10031828.html
-Advertisement-
Play Games

" 【.NET Core項目實戰 統一認證平臺】開篇及目錄索引 " 上篇文章我介紹瞭如何在網關上實現客戶端自定義限流功能,基本完成了關於網關的一些自定義擴展需求,後面幾篇將介紹基於 的認證相關知識,在具體介紹 實現我們統一認證的相關功能前,我們首先需要分析下 源碼,便於我們徹底掌握認證的原理以及後續 ...


【.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進行討論。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、前言 最近一兩個星期,加班,然後回去後弄自己的博客,把自己的電腦從 Windows 10 改到 Ubuntu 18.10 又弄回 Windows 10,原本計劃的學習 Vue 中生命周期的相關知識目前也沒有任何的進展,嗯,罪過罪過。看了眼時間,11月也快要結束了,準備補上一篇如何將我們的 .NE ...
  • const是一個c#語言的關鍵字,它限定一個變數不允許被改變 const一般修飾的變數為只讀變數 const只能在初期就使用常量初始化好,而且對也每一次編譯後的結果,const的值都是固定的 使用const在一定程度上可以提高程式的安全性和可靠性 再次賦值報錯 ...
  • 項目中有一個樹形結構的資源,需要支持搜索功能,搜索出來的結果還是需要按照樹形結構展示,下麵是簡單實現的demo。 1.首先創建TreeViewItem的ViewModel,一般情況下,樹形結構都包含DisplayName,Deepth,Parent,Children,Id, IndexCode,Vi ...
  • 因為工作需要,要把PDF的64字元串轉換為圖片的base64保存到資料庫,但是看了看國內外,一方面是做這個的比較少,還有就是做這個真的很煩. PDF轉圖片呢,大概的實現思路方式一般有兩種,一種就是重繪,類似於畫畫,把看到的畫到新的畫布上;第二種呢,就會識別裡面的內容複製到新的畫布上,我也不知道我比喻 ...
  • 找了好久,一直沒找到可用的熱力圖heatmap.js。 應該說,使用01中的語法一直都無法實現熱力圖。只能說我太菜了。。。 現在急於求成,我找了另一種語法來調用ECharts。此種語法的js文件集是從碼雲上下載下來的。GitHub也有一樣的 https://gitee.com/echarts/ech ...
  • GitHub設置使用SSH Key的好處就是可以使用SSH連接,並且提交代碼的時候可以不用輸入密碼,免密提交。SSH Key 我們使用PuTTYgen來生成公鑰(Public Key),私鑰(Private Key)和PuttyKey。在使用PuTTYgen之前,你需要先安裝TortoiseGit ...
  • 一 開發概述 對於具有一定規模的大多數企業來說,存在著這樣一種需求:存在某個或某些任務,需要系統定期,自動地執行,然而,對大多數企業來說,該技術的實現,卻是他們面臨的一大難點和挑戰。 對於大部分企業來說,實現如上功能,挑戰在哪裡? 挑戰一:如何做一個自動服務的系統? 是從0到1開發(費時費力花錢,還 ...
  • 這是我第一次發表博客。以前經常到博客園查找相關技術和代碼,今天在寫一段小程式時出現了問題, 但在網上沒能找到理想的解決方法。故註冊了博客園,想與新手分享(因為本人也不是什麼高手)。 vb.net和C#操作Oracle資料庫已經用了N多年了。由於是做工程自動化項目的,業主只對軟體的功能和 界面是否友好 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...