ASP.NET Core 預設似乎沒有提供 Basic Authentication 的 DI,但仍然可以自行實作 AuthenticationHandler
ASP.NET Core 3.1
開發環境
- VS 2019
- ASP.NET Core 3.1
- NSwag.AspNetCore.13.2.5
實作
BasicAuthenticationProvider.cs
驗證帳號密碼
這裡我建立一個假的帳號資訊,然後比對它們
public class BasicAuthenticationProvider : IBasicAuthenticationProvider
{
//模擬db存放的資料
private readonly List<User> _fakeUsers = new List<User>
{
new User
{
Id = 1, FirstName = "小章", LastName = "余", UserId = "yao", Password = "123456"
}
};
public async Task<bool> Authenticate([NotNull] string userId, [NotNull] string password)
{
return this._fakeUsers
.Where(p => string.Compare(p.UserId, userId, true) == 0)
.Where(p => p.Password == password)
.Any();
}
}
BasicAuthenticationHandler.cs
Basic Authententication
客戶端的請求要帶有 Authorization Header,Servererver 這裡要去做檢查,範例如下
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
檢查 Header:
把用戶端請求的 Parameter 轉成字串,分割,取出帳號密碼:
驗證失敗彈跳對話視窗
完整代碼如下
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IBasicAuthenticationProvider _authenticationProvider;
public BasicAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IBasicAuthenticationProvider authenticationProvider)
: base(options, logger, encoder, clock)
{
this._authenticationProvider = authenticationProvider;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!this.Request.Headers.ContainsKey("Authorization"))
{
return AuthenticateResult.Fail("Missing Authorization Header");
}
//檢查 Header
//將 Authorization Header 轉型成 AuthenticationHeaderValue 物件
AuthenticationHeaderValue.TryParse(this.Request.Headers["Authorization"], out var authenticationHeader);
if (authenticationHeader == null)
{
return AuthenticateResult.Fail("Invalid Authorization Header");
}
//只允許 Basic Authentication
if (string.Compare(authenticationHeader.Scheme, "basic", true) == 0 == false)
{
return AuthenticateResult.Fail("Only Support Basic Authority Header");
}
string userId = null;
string password = null;
try
{
//Base64 String 轉成文字,切割,取出帳號密碼
var credentialBytes = Convert.FromBase64String(authenticationHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] {':'}, 2);
userId = credentials[0];
password = credentials[1];
var isValid = await this._authenticationProvider.Authenticate(userId, password);
if (!isValid)
{
return AuthenticateResult.Fail("Invalid Username or Password");
}
}
catch (Exception)
{
return AuthenticateResult.Fail("Invalid Authority Header");
}
//建立Claim,若需要更多資訊可以從資料庫拿
var claims = new[]
{
//new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, userId)
};
var identity = new ClaimsIdentity(claims, this.Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, this.Scheme.Name);
return AuthenticateResult.Success(ticket);
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
this.Response.Headers["WWW-Authenticate"] = $"Basic realm=\"Demo APP\", charset=\"UTF-8\"";
await base.HandleChallengeAsync(properties);
}
}
Startup.cs
這裡要注意順序!(不要像我一樣)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
// Add OpenAPI/Swagger middlewares
app.UseOpenApi(); // Serves the registered OpenAPI/Swagger documents by default on `/swagger/{documentName}/swagger.json`
app.UseSwaggerUi3(); // Serves the Swagger UI 3 web ui to view the OpenAPI/Swagger documents by default on `/swagger`
}
加入自訂驗證 BasicAuthenticationHandler
注入 BasicAuthenticationProvider 給 BasicAuthenticationHandler
設定 NSwag,送出 Authorization Header,Security Key 和 Processor Name 要一樣
完整代碼如下:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddControllers();
// configure basic authentication
services.AddAuthentication("Basic")
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("Basic", null);
// configure DI for application services
//services.AddScoped<IBasicAuthenticationProvider, BasicAuthenticationProvider>();
services.AddTransient<IBasicAuthenticationProvider, BasicAuthenticationProvider>();
// Add OpenAPI v3 document
//services.AddOpenApiDocument();
services.AddOpenApiDocument(config =>
{
var apiScheme = new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.Basic,
Name = "Authorization",
In = OpenApiSecurityApiKeyLocation.Header
//Description = "Basic U3dhZ2dlcjpUZXN0"
};
config.AddSecurity("Basic", Enumerable.Empty<string>(),
apiScheme);
config.OperationProcessors
.Add(new AspNetCoreOperationSecurityScopeProcessor("Basic"));
});
// Add Swagger v2 document
// services.AddSwaggerDocument();
}
在 Controller 加上 AuthorizeAttribute
訪問 /swagger ,右上角有一個 Authorize 的按鈕,按下去,就會彈跳帳密對話視窗,這個時候還不會跑驗證
按下 Execute 時才會跑驗證
如此一來就能從 Swagger UI 送出 Authorization Header 了
換成 Postman 的效果也是一樣
擴充 Basic Authentication 的 Dependency Injection
先看結果,最終的結果如下圖,原本要注入的內容變少了,通通移到 AddBasic 擴充方法
有興趣的可以參考以下連結做法
https://joonasw.net/view/creating-auth-scheme-in-aspnet-core-2
範例位置
https://github.com/yaochangyu/sample.dotblog/tree/master/WebAPI/NSwag/Lab.DocAuth
參考
https://joonasw.net/view/creating-auth-scheme-in-aspnet-core-2
https://carsonwah.github.io/http-authentication.html
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET