理解 ASP.NET Core: 驗證
阿新 • • 發佈:2020-11-20
# ASP.NET Core 驗證
通常在應用程式中,安全分為前後兩個步驟:驗證和授權。驗證負責檢查當前請求者的身份,而授權則根據上一步得到的身份決定當前請求者是否能夠訪問期望的資源。
既然安全從驗證開始,我們也就從驗證開始介紹安全。
## 驗證的核心概念
我們先從比較簡單的場景開始考慮,例如在 Web API 開發中,需要驗證請求方是否提供了安全令牌,安全令牌是否有效。如果無效,那麼 API 端應該拒絕提供服務。在名稱空間 Microsoft.AspNetCore.Authentication 下,定義關於驗證的核心介面。對應的程式集是 Microsoft.AspNetCore.Authentication.Abstractions.dll。
### 驗證介面 IAuthenticationHandler
在 ASP.NET 下,驗證中包含 3 個基本操作:
#### Authenticate 驗證
驗證操作負責基於當前請求的上下文,使用來自請求中的資訊,例如請求頭、Cookie 等等來構造使用者標識。構建的結果是一個 AuthenticateResult 物件,它指示了驗證是否成功,如果成功的話,使用者標識將可以在驗證票據中找到。
常見的驗證包括:
* 基於 Cookie 的驗證,從請求的 Cookie 中驗證使用者
* 基於 JWT Bearer 的驗證,從請求頭中提取 JWT 令牌進行驗證
#### Challenge 質詢
在授權管理階段,如果使用者沒有得到驗證,但所期望訪問的資源要求必須得到驗證的時候,授權服務會發出質詢。例如,當匿名使用者訪問受限資源的時候,或者當用戶點選登入連結的時候。授權服務會通過質詢來相應使用者。
例如
* 基於 Cookie 的驗證會將使用者重定向到登入頁面
* 基於 JWT 的驗證會返回一個帶有 www-authenticate: bearer 響應頭的 401 響應來提醒客戶端需要提供訪問憑據
質詢操作應該讓使用者知道應該使用何種驗證機制來訪問請求的資源。
#### Forbid 拒絕
在授權管理階段,如果使用者已經通過了驗證,但是對於其訪問的資源並沒有得到許可,此時會使用拒絕操作。
例如:
* Cookie 驗證模式下,已經登入但是沒有訪問許可權的使用者,被重定向到一個提示無權訪問的頁面
* JWT 驗證模式下,返回 403
* 在自定義驗證模式下,將沒有許可權的使用者重定向到申請資源的頁面
拒絕訪問處理應該讓使用者知道:
* 它已經通過了驗證
* 但是沒有許可權訪問請求的資源
在這個場景下,可以看到,驗證需要提供的基本功能就包括了驗證和驗證失敗後的拒絕服務兩個操作。在 ASP.NET Core 中,驗證被稱為 Authenticate,拒絕被稱為 Forbid。 在供消費者訪問的網站上,如果我們希望在驗證失敗後,不是像 API 一樣直接返回一個錯誤頁面,而是將使用者導航到登入頁面,那麼,就還需要增加一個操作,這個操作的本質是希望使用者再次提供安全憑據,在 ASP.NET Core 中,這個操作被稱為 Challenge。這 3 個操作結合在一起,就是驗證最基本的要求,以介面形式表示,就是 IAuthenticationHandler 介面,如下所示:
```csharp
public interface IAuthenticationHandler
{
Task InitializeAsync(AuthenticationScheme scheme, HttpContext context);
Task AuthenticateAsync();
Task ChallengeAsync(AuthenticationProperties? properties);
Task ForbidAsync(AuthenticationProperties? properties);
}
```
驗證的結果是一個 AuthenticateResult 物件。值得注意的是,它還提供了一個靜態方法 NoResult() 用來返回沒有得到結果,靜態方法 Fail() 生成一個表示驗證異常的結果,而 Success() 成功則需要提供驗證票據。
通過驗證之後,會返回一個包含了請求者票據的驗證結果。
```csharp
namespace Microsoft.AspNetCore.Authentication
{
public class AuthenticateResult
{
// ......
public static AuthenticateResult NoResult()
{
return new AuthenticateResult() { None = true };
}
public static AuthenticateResult Fail(Exception failure)
{
return new AuthenticateResult() { Failure = failure };
}
public static AuthenticateResult Success(AuthenticationTicket ticket)
{
if (ticket == null)
{
throw new ArgumentNullException(nameof(ticket));
}
return new AuthenticateResult() { Ticket = ticket, Properties = ticket.Properties };
}
public static AuthenticateResult Success(AuthenticationTicket ticket)
{
if (ticket == null)
{
throw new ArgumentNullException(nameof(ticket));
}
return new AuthenticateResult() { Ticket = ticket, Properties = ticket.Properties };
}
// ......
}
}
```
[在 GitHub 中檢視 AuthenticateResult 原始碼](https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Http/Authentication.Abstractions/src/AuthenticateResult.cs)
那麼驗證的資訊來自哪裡呢?除了前面介紹的 3 個操作之外,還要求一個初始化的操作 Initialize,通過這個方法來提供當前請求的上下文資訊。
[在 GitHub 中檢視 IAuthenticationHandler 定義](https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Http/Authentication.Abstractions/src/IAuthenticationHandler.cs)
### 支援登入和登出操作的驗證介面
有的時候,我們還希望提供登出操作,增加登出操作的介面被稱為 IAuthenticationSignOutHandler。
```csharp
public interface IAuthenticationSignOutHandler : IAuthenticationHandler
{
Task SignOutAsync(AuthenticationProperties? properties);
}
```
[在 GitHub 中檢視 IAuthenticationSignOutHandler 原始碼](https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Http/Authentication.Abstractions/src/IAuthenticationSignOutHandler.cs)
在登出的基礎上,如果還希望提供登入操作,那麼就是 IAuthenticationSignInHandler 介面。
```csharp
public interface IAuthenticationSignInHandler : IAuthenticationSignOutHandler
{
Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties);
}
```
[在 GitHub 中檢視 IAuthenticationSignInHandler 原始碼](https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Http/Authentication.Abstractions/src/IAuthenticationSignInHandler.cs)
### 實現驗證支援的抽象基類 AuthenticationHandler
直接實現介面還是比較麻煩的,在名稱空間 Microsoft.AspNetCore.Authentication 下,微軟提供了抽象基類 AuthenticationHandler 以方便驗證控制器的開發,其它控制器可以從該控制器派生,以取得其提供的服務。
```csharp
namespace Microsoft.AspNetCore.Authentication
{
public abstract class AuthenticationHandler : IAuthenticationHandler where TOptions : AuthenticationSchemeOptions, new()
{
protected AuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
{
Logger = logger.CreateLogger(this.GetType().FullName);
UrlEncoder = encoder;
Clock = clock;
OptionsMonitor = options;
}
}
// ......
}
```
通過類的定義可以看到,它使用了泛型。每個控制器應該有一個對應該控制器的配置選項,通過泛型來指定驗證處理器所使用的配置型別,在建構函式中,可以看到它被用於獲取對應的配置選項物件。
[在 GitHub 中檢視 AuthenticationHandler 原始碼](https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Security/Authentication/Core/src/AuthenticationHandler.cs)
通過 InitializeAsync(),驗證處理器可以獲得當前請求的上下文物件 HttpContext。
```csharp
public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
```
最終,作為抽象類的 ,希望派生類來完成這個驗證任務,抽象方法 HandleAuthenticateAsync() 提供了擴充套件點。
```csharp
///
/// Allows derived types to handle authentication.
///
/// The .
protected abstract Task HandleAuthenticateAsync();
```
驗證的結果是一個 AuthenticateResult。
而拒絕服務則簡單的多,直接在這個抽象基類中提供了預設實現。直接返回 HTTP 403。
```csharp
protected virtual Task HandleForbiddenAsync(AuthenticationProperties properties)
{
Response.StatusCode = 403;
return Task.CompletedTask;
}
```
剩下的一個也一樣,提供了預設實現。直接返回 HTTP 401 響應。
```csharp
protected virtual Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.StatusCode = 401;
return Task.CompletedTask;
}
```
### Jwt 驗證處理器是如何實現的?
對於 JWT 來說,並不涉及到登入和登出,所以它需要從實現 IAuthenticationHandler 介面的抽象基類 AuthenticationHandler 派生出來即可。從 AuthenticationHandler 派生出來的 JwtBearerHandler 實現基於自己的配置選項 JwtBearerOptions。所以該類定義就變得如下所示,而建構函式顯然配合了抽象基類的要求。
```csharp
namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
public class JwtBearerHandler : AuthenticationHandler
{
public JwtBearerHandler(
IOptionsMonitor options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{ }
// ......
}
}
```
[在 GitHub 中檢視 JwtBearerHandler 原始碼](https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs)
真正的驗證則在 HandleAuthenticateAsync() 中實現。下面的程式碼是不是就很熟悉了,從請求頭中獲取附帶的 JWT 訪問令牌,然後驗證該令牌的有效性,核心程式碼如下所示。
```csharp
string authorization = Request.Headers[HeaderNames.Authorization];
// If no authorization header found, nothing to process further
if (string.IsNullOrEmpty(authorization))
{
return AuthenticateResult.NoResult();
}
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
token = authorization.Substring("Bearer ".Length).Trim();
}
// If no token found, no further work possible
if (string.IsNullOrEmpty(token))
{
return AuthenticateResult.NoResult();
}
// ......
principal = validator.ValidateToken(token, validationParameters, out validatedToken);
```
[在 GitHub 中檢視 JwtBearerHandler 原始碼](https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs)
### 註冊 Jwt 驗證處理器
在 ASP.NET Core 中,你可以使用各種驗證處理器,並不僅僅只能使用一個,驗證控制器需要一個名稱,它被看作該驗證模式 Schema 的名稱。Jwt 驗證模式的預設名稱就是 "Bearer",通過字串常量 JwtBearerDefaults.AuthenticationScheme 定義。
```csharp
namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
///
/// Default values used by bearer authentication.
///
public static class JwtBearerDefaults
{
///
/// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions
///
public const string AuthenticationScheme = "Bearer";
}
}
```
[在 GitHub 中檢視 JwtBearerDefaults 原始碼](https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Security/Authentication/JwtBearer/src/JwtBearerDefaults.cs)
最終通過 AuthenticationBuilder 的擴充套件方法 AddJwtBearer() 將 Jwt 驗證控制器註冊到依賴注入的容器中。
```csharp
public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder)
=> builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, _ => { });
public static AuthenticationBuilder AddJwtBearer(
this AuthenticationBuilder builder,
string authenticationScheme,
string displayName,
Action configureOptions)
{
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton,
JwtBearerPostConfigureOptions>());
return builder.AddScheme(
authenticationScheme, displayName, configureOptions);
}
```
[在 GitHub 中檢視 JwtBearerExtensions 擴充套件方法原始碼 ](https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs)
### 驗證架構 Schema
一種驗證處理器,加上對應的驗證配置選項,我們再為它起一個名字,組合起來就成為一種驗證架構 Schema。在 ASP.NET Core 中,可以註冊多種驗證架構。例如,授權策略可以使用架構的名稱來指定所使用的驗證架構來使用特定的驗證方式。在配置驗證的時候,通常設定預設的驗證架構。當沒有指定驗證架構的時候,就會使用預設架構進行處理。
還可以
* 對於 authenticate, challenge, 以及 forbid 操作使用不同的驗證架構
* 使用策略來組合多種驗證架構
註冊的驗證模式,最終變成 AuthenticationScheme,註冊到依賴注入服務中。
```csharp
public class AuthenticationScheme
{
public string Name { get; }
public string? DisplayName { get; }
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public Type HandlerType { get; }
}
```
[在 GitHub 中檢視 AuthenticationScheme 原始碼](https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Http/Authentication.Abstractions/src/AuthenticationScheme.cs)
## 使用驗證處理器
### IAuthenticationSchemeProvider
各種驗證架構被儲存到一個 IAuthenticationSchemeProvider 中。
```csharp
public interface IAuthenticationSchemeProvider
{
Task GetAllSchemesAsync();
Task GetSchemeAsync(string name);
void AddScheme(AuthenticationScheme scheme);
void RemoveScheme(string name);
}
```
[在 GitHub 中檢視 IAuthenticationSchemeProvider 原始碼](https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Http/Authentication.Abstractions/src/IAuthenticationSchemeProvider.cs)
## IAuthenticationHandlerProvider
最終的使用是通過 IAuthenticationHandlerProvider 來實現的,通過一個驗證模式的字串名稱,可以取得所對應的驗證控制器。
```csharp
public interface IAuthenticationHandlerProvider
{
Task GetHandlerAsync(HttpContext context, string authenticationScheme);
}
```
[在 GitHub 中檢視 IAuthenticationHandlerProvider 原始碼](https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Http/Authentication.Abstractions/src/IAuthenticationHandlerProvider.cs)
它的預設實現是 AuthenticationHandlerProvider,原始碼並不複雜。
```csharp
public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider
{
public IAuthenticationSchemeProvider Schemes { get; }
private readonly Dictionary _handlerMap
= new Dictionary(StringComparer.Ordinal);
public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes)
{
Schemes = schemes;
}
public async Task GetHandlerAsync(HttpContext context, string authenticationScheme)
{
if (_handlerMap.TryGetValue(authenticationScheme, out var value))
{
return value;
}
var scheme = await Schemes.GetSchemeAsync(authenticationScheme);
if (scheme == null)
{
return null;
}
var handler = (context.RequestServices.GetService(scheme.HandlerType) ??
ActivatorUtilities.CreateInstance(context.RequestServices, scheme.HandlerType))
as IAuthenticationHandler;
if (handler != null)
{
await handler.InitializeAsync(scheme, context);
_handlerMap[authenticationScheme] = handler;
}
return handler;
}
}
```
[在 GitHub 中檢視 AuthenticationHandlerProvider 原始碼](https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Http/Authentication.Core/src/AuthenticationHandlerProvider.cs)
## Authentication 中介軟體 AuthenticationMiddleware
驗證中介軟體的處理就沒有那麼複雜了。
找到預設的驗證模式,使用預設驗證模式的名稱取得對應的驗證處理器,如果驗證成功的話,把當前請求使用者的主體放到當前請求上下文的 User 上。
裡面還有一段特別的程式碼,用來找出哪些驗證處理器實現了 IAuthenticationHandlerProvider,並依次呼叫它們,看看是否需要提取終止請求處理過程。
```csharp
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Authentication
{
public class AuthenticationMiddleware
{
private readonly RequestDelegate _next;
public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
if (schemes == null)
{
throw new ArgumentNullException(nameof(schemes));
}
_next = next;
Schemes = schemes;
}
public IAuthenticationSchemeProvider Schemes { get; set; }
public async Task Invoke(HttpContext context)
{
context.Features.Set(new AuthenticationFeature
{
OriginalPath = context.Request.Path,
OriginalPathBase = context.Request.PathBase
});
// Give any IAuthenticationRequestHandler schemes a chance to handle the request
var handlers = context.RequestServices.GetRequiredService();
foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
{
var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
if (handler != null && await handler.HandleRequestAsync())
{
return;
}
}
var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
if (defaultAuthenticate != null)
{
var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
if (result?.Principal != null)
{
context.User = result.Principal;
}
}
await _next(context);
}
}
}
```
[在 GitHub 中檢視 AuthenticationMiddle 原始碼](https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Security/Authentication/Core/src/AuthenticationMiddleware.cs)
### 參考資料
* https://docs.microsoft.com/en-us/aspnet/core/security/authentication/?view=aspnetcore-5.0