ASP.NET Core实现基于ApiKey的认证

Intro

之前我们有介绍过实现基于请求头的认证,今天来实现一个基于 ApiKey 的认证方式,使用方式参见下面的示例.

Sample

注册认证服务

services.AddAuthentication()
    .AddApiKey(options =>
    {
        options.ApiKey = "123456";
        options.ApiKeyName = "X-ApiKey";
    });

可以根据需要是否要指定为默认的认证 Schema

控制器示例代码:

[HttpGet("apiKeyTest")]
[Authorize(AuthenticationSchemes = ApiKeyAuthenticationDefaults.AuthenticationSchema)]
public IActionResult ApiKeyAuthTest()
{
    return Ok(User.Identity);
}

因为我们没有指定为默认的认证 Schema,所以在这个 Action 上我们指定了希望使用的认证方式

调用示例:

直接调用不使用 ApiKey

ASP.NET Core实现基于ApiKey的认证

使用错误的 ApiKey

ASP.NET Core实现基于ApiKey的认证

使用正确的 ApiKey

ASP.NET Core实现基于ApiKey的认证

从上面的结果可以看出来只有在 ApiKey 是正确的情况下才能正常调用 Api

Implement

对于自定义认证方式我们之前也有介绍过基于请求头的认证方式,基于 ApiKey 的认证和请求头也类似,

对于自定义认证方式,核心需要定义的有两个类型

  • 一个是认证选项,自定义的 AuthenticationOption
  • 一个是认证处理器,自定义的认证处理逻辑

下面来看实现

ApiKeyAuthenticationOptions

实现如下:

public sealed class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
    public string ApiKey { get; set; }
    public string ApiKeyName { get; set; } = "X-ApiKey";
    public string ClientId { get; set; }
    public KeyLocation KeyLocation { get; set; }

    public override void Validate()
    {
        if (string.IsNullOrWhiteSpace(ApiKey))
        {
            throw new ArgumentException("Invalid ApiKey configured");
        }
    }
}

public enum KeyLocation
{
    Header = 0,
    Query = 1,
    HeaderOrQuery = 2,
    QueryOrHeader = 3,
}

继承于 AuthenticationSchemeOptions 并添加认证所需的配置,我们增加了一个 KeyLocation 以配置读取 ApiKey 的来源,Header 就是从请求头去读,Query 就是从查询字符串读取,HeaderOrQuery 就是优先读取 Header,对应的 QueryOrHeader 就是优先读取查询字符串

另外我们可以重载 Validate 方法来对我们的配置进行校验

ApiKeyAuthenticationHandler

认证处理器是认证方式的核心处理逻辑,我们的处理也比较简单,就是直接读取 ApiKey 与配置的 ApiKey 对比,一致就认证成功,否则就是非法请求,实现如下:

public sealed class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
    public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var authResult = HandleAuthenticateInternal();
        return Task.FromResult(authResult);
    }

    private AuthenticateResult HandleAuthenticateInternal()
    {
        StringValues keyValues;
        var keyExists = Options.KeyLocation switch
        {
            KeyLocation.Query => Request.Query.TryGetValue(Options.ApiKeyName, out keyValues),
            KeyLocation.HeaderOrQuery => Request.Headers.TryGetValue(Options.ApiKeyName, out keyValues) || Request.Query.TryGetValue(Options.ApiKeyName, out keyValues),
            KeyLocation.QueryOrHeader => Request.Query.TryGetValue(Options.ApiKeyName, out keyValues) || Request.Headers.TryGetValue(Options.ApiKeyName, out keyValues),
            _ => Request.Headers.TryGetValue(Options.ApiKeyName, out keyValues),
        };
        if (!keyExists)
            return AuthenticateResult.NoResult();

        if (keyValues.ToString().Equals(Options.ApiKey))
        {
            var clientId = Options.ClientId.GetValueOrDefault(ApplicationHelper.ApplicationName);
            return AuthenticateResult.Success(
                new AuthenticationTicket(
                    new ClaimsPrincipal(new[]
                    {
                            new ClaimsIdentity(new[]
                            {
                                new Claim(nameof(Options.ClientId), clientId, ClaimValueTypes.String, ClaimsIssuer),
                            }, Scheme.Name)
                    }), Scheme.Name)
            );
        }
        return AuthenticateResult.Fail("Invalid Api-Key");
    }
}

Extensions

为了方便使用添加了几个常用的扩展方法,类似于 ASP.NET Core 自带的认证方式

首先增加了一个默认的认证模式

public static class ApiKeyAuthenticationDefaults
{
    public const string AuthenticationSchema = "ApiKey";
}

然后增加了一些依赖注入的扩展

public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder)
{
    return builder.AddApiKey(ApiKeyAuthenticationDefaults.AuthenticationSchema);
}

public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, string schema)
{
    return builder.AddApiKey(schema, _ => { });
}

public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder,
    Action<ApiKeyAuthenticationOptions> configureOptions)
{
    return builder.AddApiKey(ApiKeyAuthenticationDefaults.AuthenticationSchema,
        configureOptions);
}

public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, string schema,
    Action<ApiKeyAuthenticationOptions> configureOptions)
{
    if (null != configureOptions)
    {
        builder.Services.Configure(configureOptions);
    }
    return builder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(schema,
        configureOptions);
}

More

目前的实现比较简单,只是对 ApiKey 做了校验,而且是直接读取的配置,大家可以根据自己需要进行一定的扩展,比如 ApiKey 从数据库中读取、增加额外的 claim 配置等。

对于一些需要配置客户端访问权限的可以使用这种方式来实现简单的认证来代替基于 OAuth 的 Client Credentials 方式,相对来说会简单一些。