ASP.NET Core 同时支持多种认证方式

前言

上次,我们实现了《ASP.NET Core 自定义认证》:.

services.AddAuthentication(option =>
{
    option.DefaultAuthenticateScheme = DemoAuthenticationOptions.Scheme;
    option.DefaultChallengeScheme = DemoAuthenticationOptions.Scheme;

})
    .AddDemoAuthentication(options => { });

我们希望同时也能支持 JWT 认证,于是又加上了AddJwtBearer

services.AddAuthentication(option =>
{
    option.DefaultAuthenticateScheme = DemoAuthenticationOptions.Scheme;
    option.DefaultChallengeScheme = DemoAuthenticationOptions.Scheme;

})
    .AddDemoAuthentication(options => { })
    .AddJwtBearer(options =>
    {
        ...
    });

但是,我们发现 JWT 认证始终不起作用,只能使用自定义认证。

猜测可能是设置了DefaultAuthenticateScheme的原因,于是去掉了相关代码:

services.AddAuthentication()
    .AddDemoAuthentication(options => { })
    .AddJwtBearer(options =>
    {
        ...
    });

这次问题更大,直接报错了:

ASP.NET Core 同时支持多种认证方式

查找原因

根据错误提示,我们找到了AuthenticationService的源码:

public virtual async Task ChallengeAsync(HttpContext context, string? scheme, AuthenticationProperties? properties)
{
    if (scheme == null)
    {
        var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync();
        scheme = defaultChallengeScheme?.Name;
        if (scheme == null)
        {
            throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
        }
    }

如果没有传入scheme,则使用DefaultChallengeScheme

继续向上查找AuthorizationMiddlewareResultHandler的源码:

public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
  {
      if (authorizeResult.Challenged)
      {
          if (policy.AuthenticationSchemes.Count > 0)
          {
              foreach (var scheme in policy.AuthenticationSchemes)
              {
                  await context.ChallengeAsync(scheme);
              }
          }

scheme来自于policy.AuthenticationSchemes

继续向上查找AuthorizationMiddleware的源码:

var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();
var policy = await AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData);

policy来自于IAuthorizeData的实现类。

那么,IAuthorizeData的实现类是谁呢:

public class AuthorizeAttribute : Attribute, IAuthorizeData

解决问题

现在,我们只需要在AuthorizeAttribute设置AuthenticationSchemes属性即可:

[HttpGet]
[Authorize(AuthenticationSchemes = "Bearer,Demo")]
public string  Get()
{
    ...
}

BearerDemo分别是JwtBearerHandlerDemoAuthenticationHandler的默认 scheme。

结论

因为可以为不同方法设置AuthorizeAttribute,因此,完全可以设置特定路由只能接受指定的认证方式:

[HttpGet("OnlyForBearer")]
[Authorize(AuthenticationSchemes = "Bearer")]
public string OnlyForBearer()
{
    ...
}

[HttpGet("OnlyForDemo")]
[Authorize(AuthenticationSchemes = "Demo")]
public string OnlyForDemo()
{
    ...
}