ASP.NET Core 有提供一些預設的 authentication scheme 可以使用,常見的有 Cookie Authentication 或是 JWT Bearer Token。當然,我們如果有其他需求也可以依照需要的效果自訂 authentication scheme 以滿足特殊規則的驗證方法。
要建立一個 authentication scheme 需要包含 scheme 的名字、AuthenticationHandler 以及 AuthenticationSchemeOptions。
假設我們需要一個 scheme,這個 scheme 的驗證方式為取得請求 Header 中鍵為 X-Foo 的值,使用該值與程式內的通關密語(bar)相比對,一樣的話就可以通過驗證。我們以這個情境自訂一個 FooBarAuthenticationHandler。
自訂 AuthenticationSchemeOptions
AuthenticationSchemeOptions 可以放入任何不想寫死在 AuthenticationHandler 裡面的設定,例如可以把 Header 的名稱及通關密語都定義在 AuthenticationSchemeOptions 中,稍後就可以將 Header 的名稱 X-Foo 及通關密語 bar 都由外部傳入。
public class FooBarAuthenticationOptions : AuthenticationSchemeOptions
{
public string HeaderName { get; set; } = null!;
public string Secret { get; set; } = null!;
}
AuthenticationHandler 的定義方式有兩種,一種是直接實作 IAuthenticationHandler
介面,另一種是繼承並實作AuthenticationHandler<TOptions>
。
直接實作介面的好處是可以少掉依賴AuthenticationHandler<TOptions>
這個虛擬類別,但是缺點就是需要自己實作所有功能,沒辦法享用 AuthenticationHandler<TOptions>
虛擬類別為我們實作好的基本功能。
如果需要完全自訂義的話會建議使用 IAuthenticationHandler
介面自行實作所有方法,而如果是有使用 AuthenticationSchemeOptions 的需求或是需要預設實作的話,建議使用繼承 AuthenticationHandler<TOptions>
的方式自訂驗證邏輯。
自訂 AuthenticationHandler
這裡使用繼承 AuthenticationHandler<TOptions>
的方式自訂 AuthenticationHandler。
首先定義一個 FooBarAuthenticationHandler 類別,該類別繼承 AuthenticationHandler<FooBarAuthenticationOptions>
並且定義建構函式初始化該類別。
public class FooBarAuthenticationHandler : AuthenticationHandler<FooBarAuthenticationOptions>
{
public FooBarAuthenticationHandler(IOptionsMonitor<FooBarAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder)
{
}
}
接著在我們自訂的 FooBarAuthenticationHandler 類別中加入 HandleAuthenticateAsync 函式,並且 override 父類別的 HandleAuthenticateAsync 方法,在使用此 handler 驗證時便會依照我們自訂義的驗證邏輯進行驗證。
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// 檢查Options中的值是否存在
if (string.IsNullOrEmpty(Options.HeaderName) || string.IsNullOrEmpty(Options.Secret))
{
throw new Exception("HeaderName and Secret are required for FooBarAuthenticationHandler.");
}
// 取得請求中特定Header的值
if (!Context.Request.Headers.TryGetValue(Options.HeaderName, out var headerStringValues))
{
return AuthenticateResult.Fail("Missing header");
}
// 檢查Header的值是否與Options中的Secret相符
if (headerStringValues.ToString() != Options.Secret)
{
return AuthenticateResult.Fail("Invalid secret");
}
var claims = new List<Claim>()
{
new Claim(GatewayAuthenticationDefaults.AuthenticationScheme, "Success")
};
var claimsIdentity = new ClaimsIdentity(claims, GatewayAuthenticationDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
return AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, GatewayAuthenticationDefaults.AuthenticationScheme));
}
HandleAuthenticateAsync 的返回型態是 AuthenticateResult,常見的方法有 Success、Fail 及 NoResult。Success 用來表示驗證成功,Fail 用來表示驗證失敗,NoResult 用來表示不成功也不失敗。
NoResult 稍微有點難以理解其作用,因為通常不是成功就是失敗,但查了一下發現有兩種情境可能會用到。第一種是假設有多個 AuthenticationHandler 可以使用,若想要讓某個 AuthenticationHandler 跳過驗證並且執行另一個驗證,這時就可以使用 NoResult。另一個情境是希望在特定條件下(例如以某個字串開頭的API)不執行某些驗證,也可以使用 NoResult 來達成此效果。
除了 HandleAuthenticateAsync 可以被 override 以外,HandleChallengeAsync 及 HandleForbiddenAsync 也可以被 override,可依照需求選擇要 override 的函式。
protected override Task HandleChallengeAsync(AuthenticationProperties? properties)
{
Context.Response.StatusCode = 401;
Context.Response.Headers.Append(
"WWW-Authenticate",
$"{Options.HeaderName} realm=\"foo\", error=\"invalid_secret\", error_description=\"Missing foo header\"");
return Task.CompletedTask;
}
protected override Task HandleForbiddenAsync(AuthenticationProperties? properties)
{
Context.Response.StatusCode = 403;
return Task.CompletedTask;
}
到這裡我們已經定義了 AuthenticationSchemeOptions 和 AuthenticationHandler,接著要定義 authentication scheme 將這些流程包裝成一個完整的驗證方案。
自訂 AuthenticationScheme
在定義 authentication scheme 時會需要賦予 scheme 一個名稱,參考微軟預設的 scheme 時發現通常會用某個 const 的字串來定義 scheme 的名稱,屆時在使用[Authorize]
attribute 時就可以直接使用該字串來指定要驗證的 scheme。
public static class FooBarAuthenticationDefaults
{
public const string AuthenticationScheme = "FooBar";
}
AuthenticationBuilder 的 AddScheme 方法需要指定 AuthenticationSchemeOptions 及 AuthenticationHandler<TOptions>
的類別,這裡我們使用 FooBarAuthenticationOptions 及 FooBarAuthenticationHandler 符合其要求。另外,需要在參數中指定 scheme 的名字及 Action<FooBarAuthenticationOptions>
的 callback function,由於包裝這層的目的是便於在 Program.cs 中呼叫,我們將 Options 的 callback 直接由參數傳入,讓呼叫者可以直接設定 Options。
public static class AuthenticationBuilderExtension
{
public static AuthenticationBuilder AddFooBar(this AuthenticationBuilder builder, Action<FooBarAuthenticationOptions> configureOptions)
=> builder.AddScheme<FooBarAuthenticationOptions, FooBarAuthenticationHandler>(FooBarAuthenticationDefaults.AuthenticationScheme, configureOptions);
}
將自訂的 AuthenticationScheme 加入到驗證服務中
在 Program.cs 中我們可以直接在 AddAuthentication() 後面加上我們自訂的 AddFooBar() 方法,並設定我們設計要傳入的設定值參數,在執行驗證時就會依照設定值進行驗證。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication()
.AddFooBar(options =>
{
options.HeaderName = "X-Foo";
options.Secret = "bar";
})
指定 AuthenticationSchemes 進行驗證
在 AuthorizeAttribute 中的 AuthenticationScheme 指定特定的 scheme 即可以該 scheme 進行驗證。
[ApiController]
[Authorize(AuthenticationSchemes = FooBarAuthenticationDefaults.AuthenticationScheme)]
public class FooBarController : ControllerBase
{
}