[ASP.NET Core] 自定義自己的 Authentication 身份驗證器

這陣子剛碰好在做內部使用的服務,試著自定義使用 Api-Key 來驗證身份,以及驗證成功時,在 User 填入對應的操作著身份

在 Startup 註冊 services.AddAuthentication() 時,會返回一個 AuthenticationBuilder,接著就要對他擴充我們自定義的處理器,這邊會使用到方法 AddScheme()

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    
    AuthenticationBuilder authenticationBuilder = services.AddAuthentication();
    authenticationBuilder.AddScheme<MyApiKeyAuthOption, MyApiKeyAuthHandler>("ApiKey", option =>
    {
    });

}

依照 AddScheme 的方法簽章可以產生兩個新類別,分別是

  1. MyApiKeyAuthOption - 自定義驗證器可使用的各種設定
  2. MyApiKeyAuthHandler - 自定義驗證器的主要處理邏輯
public class MyApiKeyAuthOption : AuthenticationSchemeOptions
{
}
public class MyApiKeyAuthHandler : AuthenticationHandler<MyApiKeyAuthOption>
{
    public MyApiKeyAuthHandler(IOptionsMonitor<MyApiKeyAuthOption> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        throw new NotImplementedException();
    }
}

接著要實作 MyApiKeyAuthHandler,這邊會定義整個自定義驗證方式的邏輯,在 HandleAuthenticateAsync 都會使用 AuthenticateResult 的方法建立返回的內容,下面的程式碼先展開了 AuthenticateResult.Success() 的參數所需要的東西

  1. AuthenticateResult.Success()
  2. AuthenticateResult.NoResult()
  3. AuthenticateResult.Fail()
public class MyApiKeyAuthHandler : AuthenticationHandler<MyApiKeyAuthOption>
{
    public MyApiKeyAuthHandler(IOptionsMonitor<MyApiKeyAuthOption> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock)
            : base(options, logger, encoder, clock)
    {
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        await Task.CompletedTask;

        var name = string.Empty;
        var role = string.Empty;

        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, name),
            new Claim(ClaimTypes.Role, role),
        };
        var claimsIdentity = new ClaimsIdentity(claims);
        var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
        var authenticationTicket = new AuthenticationTicket(claimsPrincipal, "ApiKey");
        
        return AuthenticateResult.Success(authenticationTicket);
    }
}

直接把 MyApiKeyAuthHandler 實作完成,ApiKey 應該要從DB或是其他安全的方式取得,這邊為了方便示範,直接儲存在記憶體使用。

public class MyApiKeyAuthHandler : AuthenticationHandler<MyApiKeyAuthOption>
{
    private const string _apiKeyHeaderName = "X-Api-Key";
    
    private readonly ApiKeyQuery _apiKeyQuery;

    public MyApiKeyAuthHandler(IOptionsMonitor<MyApiKeyAuthOption> options,
                               ILoggerFactory logger,
                               UrlEncoder encoder,
                               ISystemClock clock,
                               ApiKeyQuery apiKeyQuery)
        : base(options, logger, encoder, clock)
    {
        _apiKeyQuery = apiKeyQuery;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // have header "api key"?
        if (!Request.Headers.TryGetValue(_apiKeyHeaderName, out var values))
        {
            return AuthenticateResult.NoResult();
        }

        // "api key" value is empty?
        var apiKey = values.FirstOrDefault();
        if (apiKey == null || string.IsNullOrWhiteSpace(apiKey))
        {
            return AuthenticateResult.NoResult();
        }

        // api key is same?
        var apiInfo = await _apiKeyQuery.Query(apiKey);
        if (apiInfo == null)
        {
            return AuthenticateResult.Fail("invalid api key");
        }

        // generate ticket
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, apiInfo.Name),
            new Claim(ClaimTypes.Role, apiInfo.Role),
        };

        var claimsIdentity = new ClaimsIdentity(claims, MyApiKeyAuthOption.Scheme);
        var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
        var authenticationTicket = new AuthenticationTicket(claimsPrincipal, MyApiKeyAuthOption.Scheme);

        return AuthenticateResult.Success(authenticationTicket);
    }
}

public class ApiKeyQuery
{
    private readonly Dictionary<string, ApiInfo> _apiKeys =
        new Dictionary<string, ApiInfo>
        {
            ["_MyApiKey_"] = new ApiInfo("ian", "Admin"),
            ["_UserApiKey_"] = new ApiInfo("test", "User")
        };

    public Task<ApiInfo> Query(string apiKey)
    {
        if (_apiKeys.ContainsKey(apiKey))
        {
            return Task.FromResult(_apiKeys[apiKey]);
        }
        return null;
    }
}

public class ApiInfo
{
    public ApiInfo(string name, string role)
    {
        Name = name;
        Role = role;
    }

    public string Name { get; }

    public string Role { get; }
}

在執行起來測試之前,還需要在 Statup 上加入驗證檢查的 Middleware

app.UseAuthentication();

以及 Action 上的 Attribute

[ApiController]
[Route("[controller]/[action]")]
public class WeatherForecastController : ControllerBase
{
    [HttpGet]
    public string Get()
    {
        return "ok";
    }

    [HttpGet]
    [Authorize]
    public string GetAuth()
    {
        return "ok auth";
    }
}

執行後用 Postman 測試看看,可以確認ApiKey的檢查是有效的

接著確認 Role 的設定是有有效授權的

[HttpGet]
[Authorize(Roles = "Admin")]
public string GetAdmin()
{
    var name = _contextAccessor.User.Identity.Name;
    return $"ok {name}";
}

[HttpGet]
[Authorize]
public string GetAll()
{
    var name = _contextAccessor.User.Identity.Name;
    return $"ok all {name}";
}

[HttpGet]
[Authorize(Roles = "User")]
public string GetUser()
{
    var name = _contextAccessor.User.Identity.Name;
    return $"ok {name}";
}

這樣就完成了自定義的對Api-Key驗證檢查了


SampleCode