這陣子剛碰好在做內部使用的服務,試著自定義使用 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 的方法簽章可以產生兩個新類別,分別是
- MyApiKeyAuthOption - 自定義驗證器可使用的各種設定
- 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() 的參數所需要的東西
- AuthenticateResult.Success()
- AuthenticateResult.NoResult()
- 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()
{
if (!Request.Headers.TryGetValue(_apiKeyHeaderName, out var values))
{
return AuthenticateResult.NoResult();
}
var apiKey = values.FirstOrDefault();
if (apiKey == null || string.IsNullOrWhiteSpace(apiKey))
{
return AuthenticateResult.NoResult();
}
var apiInfo = await _apiKeyQuery.Query(apiKey);
if (apiInfo == null)
{
return AuthenticateResult.Fail("invalid api key");
}
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
在 Startup 註冊 services.AddAuthentication() 時,會返回一個 AuthenticationBuilder,接著就要對他擴充我們自定義的處理器,這邊會使用到方法 AddScheme()
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); AuthenticationBuilder authenticationBuilder = services.AddAuthentication(); authenticationBuilder.AddScheme<MyApiKeyAuthOption, MyApiKeyAuthHandler>("ApiKey", option => { }); }
依照 AddScheme 的方法簽章可以產生兩個新類別,分別是
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() 的參數所需要的東西
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