這兩天遇到一個應用場景,需要對內網調用的部分 web api 進行安全保護,只允許請求頭賬戶包含指定 key 的客戶端進行調用。在網上找到一篇英文博文 ASP.NET Core - Protect your API with API Keys,該文中的代碼完美基於 ASP.NET Core 內置的鑒權(Authentication) 與授權(Authorization)機制解決了這個問題,於是站在巨人的肩上自己實現了一遍,在這篇隨筆中做個記錄。
ASP.NET Core Authentication 與 Authorization 實現的開源代碼在 https://github.com/aspnet/AspNetCore/tree/master/src/Security 。
使用 API Key 對私有 Web API 進行保護,實際就是通過自定義的 AuthenticationHandler 對 ASP.NET Core Authentication 的鑒權能力進行擴展,具體實現分4步。
1)實現配角(配置選項)ApiKeyAuthenticationOptions
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "ApiKey";
public string Scheme { get; set; } = DefaultScheme;
public string AuthenticationType { get; set; } = DefaultScheme;
}
這里的示例程序很簡單,選項只用於定義 DefaultScheme 。
2)實現主角 ApiKeyAuthenticationHandler
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private const string HEADER_NAME = "X-Api-Key";
private readonly (string Owner, string Key)[] _apiKeys = new[] { ("test", "xxx123yyy456zzz") };
private readonly ApiKeyAuthenticationOptions _options;
public ApiKeyAuthenticationHandler(
IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder Encoder,
ISystemClock clock) : base(options, logger, Encoder, clock)
{
_options = options.CurrentValue;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var providedApiKey = Context.Request.Headers[HEADER_NAME].FirstOrDefault();
if (string.IsNullOrWhiteSpace(providedApiKey))
{
return AuthenticateResult.NoResult();
}
var apiKey = _apiKeys.FirstOrDefault(k => k.Key == providedApiKey);
if (apiKey != default)
{
var claims = new[]
{
new Claim(ClaimTypes.Name, apiKey.Owner)
};
var identity = new ClaimsIdentity(claims, authenticationType: _options.AuthenticationType);
var identities = new List<ClaimsIdentity> { identity };
var principal = new ClaimsPrincipal(identities);
var ticket = new AuthenticationTicket(principal, authenticationScheme: _options.Scheme);
await Task.CompletedTask;
return AuthenticateResult.Success(ticket);
}
return AuthenticateResult.Fail("Invalid API Key provided.");
}
}
在 override 方法 HandleAuthenticateAsync 中實現基於 API Key 對私有 Web API 進行保護的鑒權邏輯,根據客戶端請求頭進行驗證,如果是合法請求,就發一個 ticket 。有了這張門票, 如果只要有門票就能通過(比如加了[Authorize]聲明), 沒有其他授權要求,Authorization 就會直接放行。
3)實現跑龍套的 AuthenticationBuilderExtensions 擴展方法
public static class AuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddApiKeySupport(this AuthenticationBuilder authenticationBuilder)
{
return authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(
ApiKeyAuthenticationOptions.DefaultScheme,
options => { });
}
public static AuthenticationBuilder AddApiKeySupport(this AuthenticationBuilder authenticationBuilder,
Action<ApiKeyAuthenticationOptions> options)
{
return authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(
ApiKeyAuthenticationOptions.DefaultScheme,
options);
}
}
這個擴展方法只是為了方便在 Startup 中添加 ApiKeyAuthenticationHandler 。
4)開始演戲
Startup.ConfigureServices 中配置 Authentication
services.AddAuthentication(options =>
{
options.DefaultScheme = ApiKeyAuthenticationOptions.DefaultScheme;
options.DefaultChallengeScheme = ApiKeyAuthenticationOptions.DefaultScheme;
}).AddApiKeySupport();
Startup.Configure 中添加 Authentication 與 Authorization 中間件(注:一定要放在 app.UseRouting 之后)
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
對需要保護的 web api 添加 [Authorize] 聲明
[Authorize]
public async Task<bool> ProtectedAction()
{
//...
}
搞定。
