ASP.NET Core[源碼分析篇] - Authentication認證

  追本溯源,從使用開始  

  首先看一下咱們一般是如何使用微軟自帶的認證,通常在Startup裏面配置咱們所需的依賴認證服務,這裏經過JWT的認證方式講解緩存

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(authOpt =>
    {
        authOpt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        authOpt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(o =>
    {
        o.TokenValidationParameters = new TokenValidationParameters
        {
            //配置本身所要驗證的參數
            
        };
    });
}

  咱們來看一下源碼AddAuthentication主要作了什麼app

 public static class AuthenticationServiceCollectionExtensions
  {
    public static AuthenticationBuilder AddAuthentication( this IServiceCollection services, Action<AuthenticationOptions> configureOptions)
    {
      if (services == null)
        throw new ArgumentNullException(nameof (services));
      if (configureOptions == null)
        throw new ArgumentNullException(nameof (configureOptions));
      AuthenticationBuilder authenticationBuilder = services.AddAuthentication();
      services.Configure<AuthenticationOptions>(configureOptions);
      return authenticationBuilder;
    }

    public static AuthenticationBuilder AddAuthentication( this IServiceCollection services)
    {
      if (services == null)
        throw new ArgumentNullException(nameof (services));
      services.AddAuthenticationCore();
      services.AddDataProtection();
      services.AddWebEncoders();
      services.TryAddSingleton<ISystemClock, SystemClock>();
      return new AuthenticationBuilder(services);
    }

    public static AuthenticationBuilder AddAuthentication(
      this IServiceCollection services,
      string defaultScheme)
    {
      return services.AddAuthentication((Action<AuthenticationOptions>) (o => o.DefaultScheme = defaultScheme));
    } 

  .....
}

  ConfigureServices方法基本都是服務的註冊,基於微軟的風格,這裏的AddAuthenticationCore確定是咱們的認證服務註冊方法,來看一下async

  public static class AuthenticationCoreServiceCollectionExtensions
  {
    /// <summary>
    /// Add core authentication services needed for <see cref="T:Microsoft.AspNetCore.Authentication.IAuthenticationService" />.
    /// </summary>    
    public static IServiceCollection AddAuthenticationCore(
      this IServiceCollection services)
    {
      if (services == null)
        throw new ArgumentNullException(nameof (services));
      services.TryAddScoped<IAuthenticationService, AuthenticationService>();
      services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>();
      services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>();
      services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>();
      return services;
    }

    /// <summary>
    /// Add core authentication services needed for <see cref="T:Microsoft.AspNetCore.Authentication.IAuthenticationService" />.
    /// </summary>   
    public static IServiceCollection AddAuthenticationCore(
      this IServiceCollection services,
      Action<AuthenticationOptions> configureOptions)
    {
      if (services == null)
        throw new ArgumentNullException(nameof (services));
      if (configureOptions == null)
        throw new ArgumentNullException(nameof (configureOptions));
      services.AddAuthenticationCore();
      services.Configure<AuthenticationOptions>(configureOptions);
      return services;
    }
  }

  咱們看到這裏主要註冊了AuthenticationService, AuthenticationHandlerProvider, AuthenticationSchemeProvider這三個對象,如文章開頭所說,追本溯源,從使用開始,咱們先看一下這三個對象是如何在認證體系中使用的,且是如何發揮做用的。ide

  從使用開始

  看一下咱們的認證管道構建函數

  public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
   {
       ...
       app.UseAuthentication();
       ...
   }


 public static class AuthAppBuilderExtensions
  {
    public static IApplicationBuilder UseAuthentication( this IApplicationBuilder app)
    {
      if (app == null)
        throw new ArgumentNullException(nameof (app));
      return app.UseMiddleware<AuthenticationMiddleware>();
    }
  }

  這裏使用了約定的註冊方式UseMiddleware,而且指定使用中間件AuthenticationMiddleware  oop

 public class AuthenticationMiddleware
  {
    private readonly RequestDelegate _next;

    public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
    {
      if (next == null)
        throw new ArgumentNullException(nameof (next));
      if (schemes == null)
        throw new ArgumentNullException(nameof (schemes));
      this._next = next;
      this.Schemes = schemes;
    }

    public IAuthenticationSchemeProvider Schemes { get; set; }

    public async Task Invoke(HttpContext context)
    {
      context.Features.Set<IAuthenticationFeature>((IAuthenticationFeature) new AuthenticationFeature()
      {
        OriginalPath = context.Request.Path,
        OriginalPathBase = context.Request.PathBase
      });
      IAuthenticationHandlerProvider handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
      foreach (AuthenticationScheme authenticationScheme in await this.Schemes.GetRequestHandlerSchemesAsync())
      {
        IAuthenticationRequestHandler handlerAsync = await handlers.GetHandlerAsync(context, authenticationScheme.Name) as IAuthenticationRequestHandler;
        bool flag = handlerAsync != null;
        if (flag)
          flag = await handlerAsync.HandleRequestAsync();
        if (flag)
          return;
      }
      AuthenticationScheme authenticateSchemeAsync = await this.Schemes.GetDefaultAuthenticateSchemeAsync();
      if (authenticateSchemeAsync != null)
      {
        AuthenticateResult authenticateResult = await context.AuthenticateAsync(authenticateSchemeAsync.Name);  //實際的認證業務
        if (authenticateResult?.Principal != null) context.User = authenticateResult.Principal;
      }
      await this._next(context);
    }
  }

  在繼續往下以前,咱們先看一下這個認證中間件的做用結果,當認證經過時,在HttpContext的User屬性(ClaimPrincipal)賦予身份標識,因此在後續的請求管道中都是基於認證結果中的身份標識作鑑權,這個咱們會在後面的實際操做中會提到。源碼分析

  言歸正傳,在這裏引出了咱們的兩個對象AuthenticationHandlerProvider, AuthenticationSchemeProvider。ui

  重要對象講解

  IAuthenticationSchemeProviderthis

   從名字來看,IAuthenticationSchemeProvider的做用應該是提供Scheme的,這也是Provider在微軟的風格里面起的做用(相似於工廠模式)。spa

  這個Scheme是什麼呢?很明顯,在Framework時代,也是有基於不一樣Scheme驗證的,好比Bearer,Cookie,在Aspnet Core中定義不一樣的Scheme表明着不一樣的認證處理方式,具體體現是在每一個Scheme中包含對應的IAuthenticationHandler類型的Handler,由它來完成跟自身Scheme相關的認證處理。若是沒有定義會怎麼樣?仔細看上面這塊源碼,只有當AuthenticationScheme不爲空時纔會作認證,不然一旦在Controller打上鑑權標籤[Authorize],將會直接返回401,因此咱們必須指定本身的Scheme。

  那麼咱們在哪裏指定咱們的Scheme相似呢?咱們先返回到ConfigureService的AddJwtBearer,使用過的朋友們確定知道,這裏獲取的Scheme是咱們在ConfigureService經過Addxxx scheme指定的Scheme類型。這裏咱們是使用JWT的

  

   在這裏指定了TOptions 爲JwtBearerOptions,而THandler爲JwtBearerHandler。

     public virtual AuthenticationBuilder AddScheme<TOptions, THandler>(
      string authenticationScheme,
      string displayName,
      Action<TOptions> configureOptions)
      where TOptions : AuthenticationSchemeOptions, new()
      where THandler : AuthenticationHandler<TOptions>
    {
      return this.AddSchemeHelper<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
    }


    private AuthenticationBuilder AddSchemeHelper<TOptions, THandler>(
      string authenticationScheme,
      string displayName,
      Action<TOptions> configureOptions)
      where TOptions : class, new()
      where THandler : class, IAuthenticationHandler
    {
      this.Services.Configure<AuthenticationOptions>((Action<AuthenticationOptions>) (o => o.AddScheme(authenticationScheme, (Action<AuthenticationSchemeBuilder>) (scheme =>
      {
        scheme.HandlerType = typeof (THandler);
        scheme.DisplayName = displayName;
      }))));
      if (configureOptions != null)
        this.Services.Configure<TOptions>(authenticationScheme, configureOptions);
      this.Services.AddTransient<THandler>();
      return this;
    }

  注意這裏TOptions 是須要繼承AuthenticationSchemeOptions的,在這裏是JwtBearerOptions,而THandler是AuthenticationHandler<TOptions>類型的Handler,在這裏是JwtBearerHandler。

 

  咱們回到Scheme的分析繼續往下,首先看一下AuthenticationScheme的定義  

 public class AuthenticationScheme
  {
    /// <summary>Constructor.</summary>   
    public AuthenticationScheme(string name, string displayName, Type handlerType)
    {
      if (name == null)
        throw new ArgumentNullException(nameof (name));
      if (handlerType == (Type) null)
        throw new ArgumentNullException(nameof (handlerType));
      if (!typeof (IAuthenticationHandler).IsAssignableFrom(handlerType))
        throw new ArgumentException("handlerType must implement IAuthenticationHandler.");
      this.Name = name;
      this.HandlerType = handlerType;
      this.DisplayName = displayName;
    }

    /// <summary>The name of the authentication scheme.</summary>
    public string Name { get; }

    /// <summary>
    /// The display name for the scheme. Null is valid and used for non user facing schemes.
    /// </summary>
    public string DisplayName { get; }

    /// <summary>
    /// The <see cref="T:Microsoft.AspNetCore.Authentication.IAuthenticationHandler" /> type that handles this scheme.
    /// </summary>
    public Type HandlerType { get; }
  }

  在這裏能夠看到,若是要使用Aspnet Core自身的認證體系,需先註冊Scheme,而且該Scheme必須指定一個類型爲IAuthenticationHandler的Handler,不然會拋出異常。(這個其實在AddxxxScheme的時候已經指定了AuthenticationHandler

  咱們再看一下IAuthenticationSchemeProvider的GetRequestHandlerSchemesAsync方法作了什麼

  public virtual Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync()
    {
      return Task.FromResult<IEnumerable<AuthenticationScheme>>((IEnumerable<AuthenticationScheme>) this._requestHandlers);
    }

  這東西返回了_requestHandlers,這是什麼?看代碼

  public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider
  {
    private readonly object _lock = new object();
    private readonly AuthenticationOptions _options;
    private readonly IDictionary<string, AuthenticationScheme> _schemes;
    private readonly List<AuthenticationScheme> _requestHandlers;

    /// <summary>
    /// Creates an instance of <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider" />
    /// using the specified <paramref name="options" />,
    /// </summary>   
    public AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options)
      : this(options, (IDictionary<string, AuthenticationScheme>) new Dictionary<string, AuthenticationScheme>((IEqualityComparer<string>) StringComparer.Ordinal))
    {
    }

    /// <summary>
    /// Creates an instance of <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider" />
    /// using the specified <paramref name="options" /> and <paramref name="schemes" />.
    /// </summary>   
    protected AuthenticationSchemeProvider(
      IOptions<AuthenticationOptions> options,
      IDictionary<string, AuthenticationScheme> schemes)
    {
      this._options = options.Value;
      IDictionary<string, AuthenticationScheme> dictionary = schemes;
      if (dictionary == null)
        throw new ArgumentNullException(nameof (schemes));
      this._schemes = dictionary;
      this._requestHandlers = new List<AuthenticationScheme>();
      foreach (AuthenticationSchemeBuilder scheme in this._options.Schemes)
        this.AddScheme(scheme.Build());
    }

  public virtual void AddScheme(AuthenticationScheme scheme)
    {
      if (this._schemes.ContainsKey(scheme.Name))
        throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
      lock (this._lock)
      {
        if (this._schemes.ContainsKey(scheme.Name))
          throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
        if (typeof (IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))
          this._requestHandlers.Add(scheme);
        this._schemes[scheme.Name] = scheme;
      }
    }
.....
}

  這東西就是把咱們在認證註冊服務中指定的scheme,經過解析出的AuthenticationSchemeProvider 的構造函數加載來的,進而返回一系列的List<AuthenticationScheme>,OK拿到這些scheme以後有什麼用呢?這裏引出了咱們的第二個對象AuthenticationHandlerProvider,下面咱們來了解一下。   

  IAuthenticationHandlerProvider

   咱們看到,AuthenticationMiddleware中用到了IAuthenticationHandlerProvider的GetHandlerAsync方法,那咱們先看一下這個方法的做用

public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider
  {
    private Dictionary<string, IAuthenticationHandler> _handlerMap = new Dictionary<string, IAuthenticationHandler>((IEqualityComparer<string>) StringComparer.Ordinal);

    /// <summary>Constructor.</summary>
    public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes)
    {
      this.Schemes = schemes;
    }

    /// <summary>
    /// The <see cref="T:Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider" />.
    /// </summary>
    public IAuthenticationSchemeProvider Schemes { get; }

    /// <summary>Returns the handler instance that will be used.</summary>   
    public async Task<IAuthenticationHandler> GetHandlerAsync( HttpContext context, string authenticationScheme)
    {
      if (this._handlerMap.ContainsKey(authenticationScheme))
        return this._handlerMap[authenticationScheme];
      AuthenticationScheme schemeAsync = await this.Schemes.GetSchemeAsync(authenticationScheme);
      if (schemeAsync == null)
        return (IAuthenticationHandler) null;
      IAuthenticationHandler handler = (context.RequestServices.GetService(schemeAsync.HandlerType) ?? ActivatorUtilities.CreateInstance(context.RequestServices, schemeAsync.HandlerType)) as IAuthenticationHandler;
      if (handler != null)
      {
        await handler.InitializeAsync(schemeAsync, context);
        this._handlerMap[authenticationScheme] = handler;
      }
      return handler;
    }
  }  

  在建立Handler的時候,是先從AuthenticationScheme中獲取,若是不存在則經過ActivatorUtilities建立。 獲取到Handle後,將會放在_handlerMap字典裏面, 當下次獲取Handler的時候,將直接從緩存中獲取。

  IAuthenticationService

   這個對象是在AuthenticationMiddleware中最後纔用到的,並且是基於HttpContext的擴展被調用

public static class AuthenticationHttpContextExtensions
{
    public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) =>
        context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme);

  ....          
}

  這裏主要調用了IAuthenticationService的AuthenticateAsync方法,看一下這個方法作了什麼

public class AuthenticationService : IAuthenticationService
{
    public IAuthenticationSchemeProvider Schemes { get; }
    public IAuthenticationHandlerProvider Handlers { get; }
    public IClaimsTransformation Transform { get; }

    public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
    {
        if (scheme == null)
        {
            var scheme = (await this.Schemes.GetDefaultAuthenticateSchemeAsync())?.Name;
            if (scheme == null)
                throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found.");
        }

        var handler = await Handlers.GetHandlerAsync(context, scheme);
        if(handler == null)
            throw await this.CreateMissingHandlerException(scheme);
        AuthenticateResult result = await handler.AuthenticateAsync();
        if (result != null && result.Succeeded)           
            return AuthenticateResult.Success(new AuthenticationTicket(await Transform.TransformAsync(result.Principal), result.Properties, result.Ticket.AuthenticationScheme));

        return result;
    }
}

   這裏其實就是咱們在前面講的根據Scheme獲取對應的AuthenticationHandler,而後調用AuthenticateAsync()方法,這個方法調用了核心方法HandleAuthenticateOnceAsync,而後再調用HandleAuthenticateAsync()這個核心的認證方法。

  從上圖看到這個HandleAuthenticateAsync是個抽象方法,咱們的子類都須要實現這個方法的動做,基於本文的例子,咱們看一下JwtBearerHandler的一個實際認證。  

public class JwtBearerHandler : AuthenticationHandler<JwtBearerOptions>
{
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
      JwtBearerHandler jwtBearerHandler = this;
      string token = (string) null;
      object obj;
      AuthenticationFailedContext authenticationFailedContext;
      int num;
      try
      {
        MessageReceivedContext messageReceivedContext = new MessageReceivedContext(jwtBearerHandler.Context, jwtBearerHandler.Scheme, jwtBearerHandler.Options);
        await jwtBearerHandler.Events.MessageReceived(messageReceivedContext);
        if (messageReceivedContext.Result != null)
          return messageReceivedContext.Result;
        token = messageReceivedContext.Token;
        if (string.IsNullOrEmpty(token))
        {
          string header = (string) jwtBearerHandler.Request.Headers["Authorization"];
          if (string.IsNullOrEmpty(header))
            return AuthenticateResult.NoResult();
          if (header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
            token = header.Substring("Bearer ".Length).Trim();
          if (string.IsNullOrEmpty(token))
            return AuthenticateResult.NoResult();
        }
        if (jwtBearerHandler._configuration == null && jwtBearerHandler.Options.ConfigurationManager != null)
        {
          OpenIdConnectConfiguration configurationAsync = await jwtBearerHandler.Options.ConfigurationManager.GetConfigurationAsync(jwtBearerHandler.Context.RequestAborted);
          jwtBearerHandler._configuration = configurationAsync;
        }
        TokenValidationParameters validationParameters1 = jwtBearerHandler.Options.TokenValidationParameters.Clone();
        if (jwtBearerHandler._configuration != null)
        {
          string[] strArray = new string[1]
          {
            jwtBearerHandler._configuration.Issuer
          };
          TokenValidationParameters validationParameters2 = validationParameters1;
          IEnumerable<string> validIssuers = validationParameters1.get_ValidIssuers();
          object obj1 = (validIssuers != null ? (object) validIssuers.Concat<string>((IEnumerable<string>) strArray) : (object) null) ?? (object) strArray;
          validationParameters2.set_ValidIssuers((IEnumerable<string>) obj1);
          TokenValidationParameters validationParameters3 = validationParameters1;
          IEnumerable<SecurityKey> issuerSigningKeys = validationParameters1.get_IssuerSigningKeys();
          IEnumerable<SecurityKey> securityKeys = (issuerSigningKeys != null ? issuerSigningKeys.Concat<SecurityKey>((IEnumerable<SecurityKey>) jwtBearerHandler._configuration.get_SigningKeys()) : (IEnumerable<SecurityKey>) null) ?? (IEnumerable<SecurityKey>) jwtBearerHandler._configuration.get_SigningKeys();
          validationParameters3.set_IssuerSigningKeys(securityKeys);
        }
        List<Exception> exceptionList = (List<Exception>) null;
        foreach (ISecurityTokenValidator securityTokenValidator in (IEnumerable<ISecurityTokenValidator>) jwtBearerHandler.Options.SecurityTokenValidators)
        {
          if (securityTokenValidator.CanReadToken(token))
          {
            SecurityToken securityToken;
            ClaimsPrincipal claimsPrincipal;
            try
            {
              claimsPrincipal = securityTokenValidator.ValidateToken(token, validationParameters1, ref securityToken);
            }
            catch (Exception ex)
            {
              jwtBearerHandler.Logger.TokenValidationFailed(ex);
              if (jwtBearerHandler.Options.RefreshOnIssuerKeyNotFound && jwtBearerHandler.Options.ConfigurationManager != null && ex is SecurityTokenSignatureKeyNotFoundException)
                jwtBearerHandler.Options.ConfigurationManager.RequestRefresh();
              if (exceptionList == null)
                exceptionList = new List<Exception>(1);
              exceptionList.Add(ex);
              continue;
            }
            jwtBearerHandler.Logger.TokenValidationSucceeded();
            TokenValidatedContext validatedContext = new TokenValidatedContext(jwtBearerHandler.Context, jwtBearerHandler.Scheme, jwtBearerHandler.Options);
            validatedContext.Principal = claimsPrincipal;
            validatedContext.SecurityToken = securityToken;
            TokenValidatedContext tokenValidatedContext = validatedContext;
            await jwtBearerHandler.Events.TokenValidated(tokenValidatedContext);
            if (tokenValidatedContext.Result != null)
              return tokenValidatedContext.Result;
            if (jwtBearerHandler.Options.SaveToken)
              tokenValidatedContext.Properties.StoreTokens((IEnumerable<AuthenticationToken>) new AuthenticationToken[1]
              {
                new AuthenticationToken()
                {
                  Name = "access_token",
                  Value = token
                }
              });
            tokenValidatedContext.Success();
            return tokenValidatedContext.Result;
          }
        }
        if (exceptionList == null)
          return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]");
        authenticationFailedContext = new AuthenticationFailedContext(jwtBearerHandler.Context, jwtBearerHandler.Scheme, jwtBearerHandler.Options)
        {
          Exception = exceptionList.Count == 1 ? exceptionList[0] : (Exception) new AggregateException((IEnumerable<Exception>) exceptionList)
        };
        await jwtBearerHandler.Events.AuthenticationFailed(authenticationFailedContext);
        return authenticationFailedContext.Result == null ? AuthenticateResult.Fail(authenticationFailedContext.Exception) : authenticationFailedContext.Result;
      }
      catch (Exception ex)
      {
        obj = (object) ex;
        num = 1;
      }
      if (num == 1)
      {
        Exception ex = (Exception) obj;
        jwtBearerHandler.Logger.ErrorProcessingMessage(ex);
        authenticationFailedContext = new AuthenticationFailedContext(jwtBearerHandler.Context, jwtBearerHandler.Scheme, jwtBearerHandler.Options)
        {
          Exception = ex
        };
        await jwtBearerHandler.Events.AuthenticationFailed(authenticationFailedContext);
        if (authenticationFailedContext.Result != null)
          return authenticationFailedContext.Result;
        Exception source = obj as Exception;
        if (source == null)
          throw obj;
        ExceptionDispatchInfo.Capture(source).Throw();
        authenticationFailedContext = (AuthenticationFailedContext) null;
      }
      obj = (object) null;
      token = (string) null;
      AuthenticateResult authenticateResult;
      return authenticateResult;
    }
}

   這個方法有點長,主要是從Request.Headers裏面獲取Authorization的Bearer出來解析,再在AddJwtBearer中傳入的委託參數JwtBearerOptions的TokenValidationParameters屬性做爲依據進行對比來進行認證是否經過與否。

  總結

  本文對 ASP.NET Core 的認證流程作了一個源碼分析流程介紹,因爲是源碼分析篇,因此可能會比較枯燥和苦澀難懂。在後面的真正使用過程當中,而後再結合本篇的一個總結流程,相信你們會逐漸開朗。

  1. 在Startup類中的ConfigureServices方法經過添加AddAuthentication註冊咱們最主要的三個對象AuthenticationService, AuthenticationHandlerProvider, AuthenticationSchemeProvider
  2. 經過AddAuthentication返回的AuthenticationBuilder 經過AddJwtBearer(或者AddCookie)來指定Scheme類型和須要驗證的參數
  3. 在Startup類中的Configure方法經過添加UseAuthentication註冊認證中間件
  4. 在認證過程當中,經過AuthenticationSchemeProvider獲取正確的Scheme,AuthenticationService中經過Scheme和AuthenticationHandlerProvider獲取正確的AuthenticationHandler,最後經過對應的AuthenticationHandler的AuthenticateAsync方法進行認證流程
相關文章
相關標籤/搜索