API Key 和 JWT Token 可以在同一个 .Net 6 WebAPI 中使用吗

Jim*_*ans 3 api-key jwt asp.net-web-api .net-6.0

我正在构建一个新的 .Net 6 WebAPI,它将被许多应用程序使用,因此我需要实现 API 密钥来限制仅对这些应用程序的访问。只有极少数的个人用户需要授权(管理员),因此我想将管理端点与 JWT 结合起来。我们不希望要求用户在不必要的情况下(非管理员)必须创建帐户。这可能吗?谢谢。

ale*_*e91 8

对的,这是可能的。
我建议的解决方案是在 ASP.NET Core 6 中使用必须指定内部Authorize属性的两种身份验证方案来设置多种身份验证方法。下面是ApiKey认证的简单实现:

namespace MyAuthentication;

public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
    private enum AuthenticationFailureReason
    {
        NONE = 0,
        API_KEY_HEADER_NOT_PROVIDED,
        API_KEY_HEADER_VALUE_NULL,
        API_KEY_INVALID
    }

    private readonly Microsoft.Extensions.Logging.ILogger _logger;

    private AuthenticationFailureReason _failureReason = AuthenticationFailureReason.NONE;

    public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options,
                                       ILoggerFactory loggerFactory,
                                       ILogger<ApiKeyAuthenticationHandler> logger,
                                       UrlEncoder encoder,
                                       ISystemClock clock) : base(options, loggerFactory, encoder, clock)
    {
        _logger = logger;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        //ApiKey header get
        if (!TryGetApiKeyHeader(out string providedApiKey, out AuthenticateResult authenticateResult))
        {
            return authenticateResult;
        }

        //TODO: you apikey validity check
        if (await ApiKeyCheckAsync(providedApiKey))
        {
            var principal = new ClaimsPrincipal();  //TODO: Create your Identity retreiving claims
            var ticket = new AuthenticationTicket(principal, ApiKeyAuthenticationOptions.Scheme);

            return AuthenticateResult.Success(ticket);
        }

        _failureReason = AuthenticationFailureReason.API_KEY_INVALID;
        return AuthenticateResult.NoResult();
    }

    protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        //Create response
        Response.Headers.Append(HeaderNames.WWWAuthenticate, $@"Authorization realm=""{ApiKeyAuthenticationOptions.DefaultScheme}""");
        Response.StatusCode = StatusCodes.Status401Unauthorized;
        Response.ContentType = MediaTypeNames.Application.Json;

        //TODO: setup a response to provide additional information if you want
        var result = new
        {
            StatusCode = Response.StatusCode,
            Message = _failureReason switch
            {
                AuthenticationFailureReason.API_KEY_HEADER_NOT_PROVIDED => "ApiKey not provided",
                AuthenticationFailureReason.API_KEY_HEADER_VALUE_NULL => "ApiKey value is null",
                AuthenticationFailureReason.NONE or AuthenticationFailureReason.API_KEY_INVALID or _ => "ApiKey is not valid"
            }
        };

        using var responseStream = new MemoryStream();
        await JsonSerializer.SerializeAsync(responseStream, result);
        await Response.BodyWriter.WriteAsync(responseStream.ToArray());
    }

    protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
    {
        //Create response
        Response.Headers.Append(HeaderNames.WWWAuthenticate, $@"Authorization realm=""{ApiKeyAuthenticationOptions.DefaultScheme}""");
        Response.StatusCode = StatusCodes.Status403Forbidden;
        Response.ContentType = MediaTypeNames.Application.Json;

        var result = new
        {
            StatusCode = Response.StatusCode,
            Message = "Forbidden"
        };

        using var responseStream = new MemoryStream();
        await JsonSerializer.SerializeAsync(responseStream, result);
        await Response.BodyWriter.WriteAsync(responseStream.ToArray());
    }

    #region Privates
    private bool TryGetApiKeyHeader(out string apiKeyHeaderValue, out AuthenticateResult result)
    {
        apiKeyHeaderValue = null;
        if (!Request.Headers.TryGetValue("X-Api-Key", out var apiKeyHeaderValues))
        {
            _logger.LogError("ApiKey header not provided");

            _failureReason = AuthenticationFailureReason.API_KEY_HEADER_NOT_PROVIDED;
            result = AuthenticateResult.Fail("ApiKey header not provided");

            return false;
        }

        apiKeyHeaderValue = apiKeyHeaderValues.FirstOrDefault();
        if (apiKeyHeaderValues.Count == 0 || string.IsNullOrWhiteSpace(apiKeyHeaderValue))
        {
            _logger.LogError("ApiKey header value null");

            _failureReason = AuthenticationFailureReason.API_KEY_HEADER_VALUE_NULL;
            result = AuthenticateResult.Fail("ApiKey header value null");

            return false;
        }

        result = null;
        return true;
    }

    private Task<bool> ApiKeyCheckAsync(string apiKey)
    {
        //TODO: setup your validation code...

        return Task.FromResult<bool>(true);
    }
    #endregion
}

public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
    public const string DefaultScheme = "ApiKey";

    public static string Scheme => DefaultScheme;
    public static string AuthenticationType => DefaultScheme;
}

public static class AuthenticationBuilderExtensions
{
    public static AuthenticationBuilder AddApiKeySupport(this AuthenticationBuilder authenticationBuilder, Action<ApiKeyAuthenticationOptions> options)
        => authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationOptions.DefaultScheme, options);
}
Run Code Online (Sandbox Code Playgroud)

然后在构建器设置中注册:

_ = services.AddAuthentication(options =>
             {
                options.DefaultAuthenticateScheme = ApiKeyAuthenticationOptions.DefaultScheme;
                options.DefaultChallengeScheme = ApiKeyAuthenticationOptions.DefaultScheme;
             })
             .AddApiKeySupport(options => { });
Run Code Online (Sandbox Code Playgroud)

您还必须设置标准 JWT 承载验证(为了简洁起见,我不发布它)。

为了保护您的端点,请添加Authorize如下属性:

[Authorize(AuthenticationSchemes = ApiKeyAuthenticationOptions.DefaultScheme)]  //ApiKey
[HttpGet]
public async Task<IActionResult> Get()
{
   //...omissis...

   return null;
}

//or..

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] //Jwt
[HttpGet]
public async Task<IActionResult> Get()
{
   //...omissis...

   return null;
}

//or..
[Authorize(AuthenticationSchemes = $"{JwtBearerDefaults.AuthenticationScheme},{ApiKeyAuthenticationOptions.DefaultScheme}" )] //ApiKey and Jwt
[HttpGet]
public async Task<IActionResult> Get()
{
   //...omissis...

   return null;
}
Run Code Online (Sandbox Code Playgroud)

对我来说,这是在应用程序管道启动之前执行授权检查(快速失败)并能够创建用户身份的最佳方法。

但是,如果您不需要将有关 Api Key 的信息放入其中ClaimsPrincipal,而只检查 Api Key 的有效性,最简单的方法是:

  • Authorize使用 JWT 身份验证(带属性)保护“管理”操作
  • 设置并注册一个中间件以仅在所有操作中检查 Api Key 这是一个示例:
public class SimpleApiKeyMiddleware
{
    private static readonly string API_KEY_HEADER = "X-Api-Key";

    private readonly RequestDelegate _next;
    private readonly ILogger<SimpleApiKeyMiddleware> _logger;

    public SimpleApiKeyMiddleware(RequestDelegate next, ILogger<SimpleApiKeyMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        //Get apikey header
        if (!httpContext.Request.Headers.TryGetValue(API_KEY_HEADER, out var apiKey))
        {
            _logger.LogError("ApiKey not found inside request headers");

            //Error and exit from asp.net core pipeline
            await GenerateForbiddenResponse(httpContext, "ApiKey not found inside request headers");
        }
        else if (!await ApiKeyCheckAsync(apiKey))
        {
            _logger.LogError("ApiKey is not valid: {ApiKey}", apiKey);

            //Error and exit from asp.net core pipeline
            await GenerateForbiddenResponse(httpContext, "ApiKey not valid");
        }
        else
        {
            _logger.LogInformation("ApiKey validated: {ApiKey}", apiKey);

            //Proceed with pipeline
            await _next(httpContext);
        }
    }

    private Task<bool> ApiKeyCheckAsync(string apiKey)
    {
        //TODO: setup your validation code...

        return Task.FromResult<bool>(true);
    }

    private async Task GenerateForbiddenResponse(HttpContext context, string message)
    {
        context.Response.StatusCode = StatusCodes.Status403Forbidden;
        context.Response.ContentType = MediaTypeNames.Application.Json;

        using var responseStream = new MemoryStream();
        await System.Text.Json.JsonSerializer.SerializeAsync(responseStream, new
        {
            Status = StatusCodes.Status403Forbidden,
            Message = message
        });

        await context.Response.BodyWriter.WriteAsync(responseStream.ToArray());
    }
}
Run Code Online (Sandbox Code Playgroud)

登记:

_ = app.UseMiddleware<ApiKeyMiddleware>();  //Register as first middleware to avoid other middleware execution before api key check
Run Code Online (Sandbox Code Playgroud)

用法:

//Admin: Jwt and Api Key check
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] //Jwt and Api Key
[HttpGet]
public async Task<IActionResult> MyAdminApi()
{
   //...omissis...
}

//Non Admin: Api Key check only
[HttpGet]
public async Task<IActionResult> MyNonAdminApi()
{
   //...omissis...
}
Run Code Online (Sandbox Code Playgroud)

注意:上面的中间件代码强制从管道退出返回http结果,以停止下一个中间件的执行。另请注意,asp.net core 6 管道Authorization首先执行,然后执行所有注册的中间件。