深入理解 ASP.NET Core: 認(rèn)證

ASP.NET Core 認(rèn)證
通常在應(yīng)用程序中,安全分為前后兩個(gè)步驟:驗(yàn)證和授權(quán)。驗(yàn)證負(fù)責(zé)檢查當(dāng)前請(qǐng)求者的身份,而授權(quán)則根據(jù)上一步得到的身份決定當(dāng)前請(qǐng)求者是否能夠訪問(wèn)期望的資源。
既然安全從驗(yàn)證開始,我們也就從驗(yàn)證開始介紹安全。
驗(yàn)證的核心概念
我們先從比較簡(jiǎn)單的場(chǎng)景開始考慮,例如在 Web API 開發(fā)中,需要驗(yàn)證請(qǐng)求方是否提供了安全令牌,安全令牌是否有效。如果無(wú)效,那么 API 端應(yīng)該拒絕提供服務(wù)。在命名空間 Microsoft.AspNetCore.Authentication 下,定義關(guān)于驗(yàn)證的核心接口。對(duì)應(yīng)的程序集是 Microsoft.AspNetCore.Authentication.Abstractions.dll。
驗(yàn)證接口 IAuthenticationHandler
在 ASP.NET 下,驗(yàn)證中包含 3 個(gè)基本操作:
Authenticate 驗(yàn)證
驗(yàn)證操作負(fù)責(zé)基于當(dāng)前請(qǐng)求的上下文,使用來(lái)自請(qǐng)求中的信息,例如請(qǐng)求頭、Cookie 等等來(lái)構(gòu)造用戶標(biāo)識(shí)。構(gòu)建的結(jié)果是一個(gè) AuthenticateResult 對(duì)象,它指示了驗(yàn)證是否成功,如果成功的話,用戶標(biāo)識(shí)將可以在驗(yàn)證票據(jù)中找到。
常見(jiàn)的驗(yàn)證包括:
基于 Cookie 的驗(yàn)證,從請(qǐng)求的 Cookie 中驗(yàn)證用戶
基于 JWT Bearer 的驗(yàn)證,從請(qǐng)求頭中提取 JWT 令牌進(jìn)行驗(yàn)證
Challenge 質(zhì)詢
在授權(quán)管理階段,如果用戶沒(méi)有得到驗(yàn)證,但所期望訪問(wèn)的資源要求必須得到驗(yàn)證的時(shí)候,授權(quán)服務(wù)會(huì)發(fā)出質(zhì)詢。例如,當(dāng)匿名用戶訪問(wèn)受限資源的時(shí)候,或者當(dāng)用戶點(diǎn)擊登錄鏈接的時(shí)候。授權(quán)服務(wù)會(huì)通過(guò)質(zhì)詢來(lái)相應(yīng)用戶。
例如
基于 Cookie 的驗(yàn)證會(huì)將用戶重定向到登錄頁(yè)面
基于 JWT 的驗(yàn)證會(huì)返回一個(gè)帶有 www-authenticate: bearer 響應(yīng)頭的 401 響應(yīng)來(lái)提醒客戶端需要提供訪問(wèn)憑據(jù)
質(zhì)詢操作應(yīng)該讓用戶知道應(yīng)該使用何種驗(yàn)證機(jī)制來(lái)訪問(wèn)請(qǐng)求的資源。
Forbid 拒絕
在授權(quán)管理階段,如果用戶已經(jīng)通過(guò)了驗(yàn)證,但是對(duì)于其訪問(wèn)的資源并沒(méi)有得到許可,此時(shí)會(huì)使用拒絕操作。
例如:
Cookie 驗(yàn)證模式下,已經(jīng)登錄但是沒(méi)有訪問(wèn)權(quán)限的用戶,被重定向到一個(gè)提示無(wú)權(quán)訪問(wèn)的頁(yè)面
JWT 驗(yàn)證模式下,返回 403
在自定義驗(yàn)證模式下,將沒(méi)有權(quán)限的用戶重定向到申請(qǐng)資源的頁(yè)面
拒絕訪問(wèn)處理應(yīng)該讓用戶知道:
它已經(jīng)通過(guò)了驗(yàn)證
但是沒(méi)有權(quán)限訪問(wèn)請(qǐng)求的資源
在這個(gè)場(chǎng)景下,可以看到,驗(yàn)證需要提供的基本功能就包括了驗(yàn)證和驗(yàn)證失敗后的拒絕服務(wù)兩個(gè)操作。在 ASP.NET Core 中,驗(yàn)證被稱為 Authenticate,拒絕被稱為 Forbid。在供消費(fèi)者訪問(wèn)的網(wǎng)站上,如果我們希望在驗(yàn)證失敗后,不是像 API 一樣直接返回一個(gè)錯(cuò)誤頁(yè)面,而是將用戶導(dǎo)航到登錄頁(yè)面,那么,就還需要增加一個(gè)操作,這個(gè)操作的本質(zhì)是希望用戶再次提供安全憑據(jù),在 ASP.NET Core 中,這個(gè)操作被稱為 Challenge。這 3 個(gè)操作結(jié)合在一起,就是驗(yàn)證最基本的要求,以接口形式表示,就是 IAuthenticationHandler 接口,如下所示:
public interface IAuthenticationHandler
{
Task InitializeAsync(AuthenticationScheme scheme, HttpContext context);
Task AuthenticateAsync();
Task ChallengeAsync(AuthenticationProperties? properties);
Task ForbidAsync(AuthenticationProperties? properties);
}
驗(yàn)證的結(jié)果是一個(gè) AuthenticateResult 對(duì)象。值得注意的是,它還提供了一個(gè)靜態(tài)方法 NoResult() 用來(lái)返回沒(méi)有得到結(jié)果,靜態(tài)方法 Fail() 生成一個(gè)表示驗(yàn)證異常的結(jié)果,而 Success() 成功則需要提供驗(yàn)證票據(jù)。
通過(guò)驗(yàn)證之后,會(huì)返回一個(gè)包含了請(qǐng)求者票據(jù)的驗(yàn)證結(jié)果。
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 源碼
那么驗(yàn)證的信息來(lái)自哪里呢?除了前面介紹的 3 個(gè)操作之外,還要求一個(gè)初始化的操作 Initialize,通過(guò)這個(gè)方法來(lái)提供當(dāng)前請(qǐng)求的上下文信息。
在 GitHub 中查看 IAuthenticationHandler 定義
支持登錄和登出操作的驗(yàn)證接口
有的時(shí)候,我們還希望提供登出操作,增加登出操作的接口被稱為 IAuthenticationSignOutHandler。
public interface IAuthenticationSignOutHandler : IAuthenticationHandler
{
Task SignOutAsync(AuthenticationProperties? properties);
}
在 GitHub 中查看 IAuthenticationSignOutHandler 源碼
在登出的基礎(chǔ)上,如果還希望提供登錄操作,那么就是 IAuthenticationSignInHandler 接口。
public interface IAuthenticationSignInHandler : IAuthenticationSignOutHandler
{
Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties);
}
在 GitHub 中查看 IAuthenticationSignInHandler 源碼
實(shí)現(xiàn)驗(yàn)證支持的抽象基類 AuthenticationHandler
直接實(shí)現(xiàn)接口還是比較麻煩的,在命名空間 Microsoft.AspNetCore.Authentication 下,微軟提供了抽象基類 AuthenticationHandler 以方便驗(yàn)證控制器的開發(fā),其它控制器可以從該控制器派生,以取得其提供的服務(wù)。
namespace Microsoft.AspNetCore.Authentication
{
public abstract class AuthenticationHandler<TOptions> : 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;
}
}
// ......
}
通過(guò)類的定義可以看到,它使用了泛型。每個(gè)控制器應(yīng)該有一個(gè)對(duì)應(yīng)該控制器的配置選項(xiàng),通過(guò)泛型來(lái)指定驗(yàn)證處理器所使用的配置類型,在構(gòu)造函數(shù)中,可以看到它被用于獲取對(duì)應(yīng)的配置選項(xiàng)對(duì)象。
在 GitHub 中查看 AuthenticationHandler 源碼
通過(guò) InitializeAsync(),驗(yàn)證處理器可以獲得當(dāng)前請(qǐng)求的上下文對(duì)象 HttpContext。
public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
最終,作為抽象類的 ,希望派生類來(lái)完成這個(gè)驗(yàn)證任務(wù),抽象方法 HandleAuthenticateAsync() 提供了擴(kuò)展點(diǎn)。
///
/// Allows derived types to handle authentication.
///
/// The .
protected abstract Task HandleAuthenticateAsync();
驗(yàn)證的結(jié)果是一個(gè) AuthenticateResult。
而拒絕服務(wù)則簡(jiǎn)單的多,直接在這個(gè)抽象基類中提供了默認(rèn)實(shí)現(xiàn)。直接返回 HTTP 403。
protected virtual Task HandleForbiddenAsync(AuthenticationProperties properties)
{
Response.StatusCode = 403;
return Task.CompletedTask;
}
剩下的一個(gè)也一樣,提供了默認(rèn)實(shí)現(xiàn)。直接返回 HTTP 401 響應(yīng)。
protected virtual Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.StatusCode = 401;
return Task.CompletedTask;
}
Jwt 驗(yàn)證處理器是如何實(shí)現(xiàn)的?
對(duì)于 JWT 來(lái)說(shuō),并不涉及到登入和登出,所以它需要從實(shí)現(xiàn) IAuthenticationHandler 接口的抽象基類 AuthenticationHandler 派生出來(lái)即可。從 AuthenticationHandler 派生出來(lái)的 JwtBearerHandler 實(shí)現(xiàn)基于自己的配置選項(xiàng) JwtBearerOptions。所以該類定義就變得如下所示,而構(gòu)造函數(shù)顯然配合了抽象基類的要求。
namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
public class JwtBearerHandler : AuthenticationHandler<JwtBearerOptions>
{
public JwtBearerHandler(
IOptionsMonitor options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{ }
// ......
}
}
在 GitHub 中查看 JwtBearerHandler 源碼
真正的驗(yàn)證則在 HandleAuthenticateAsync() 中實(shí)現(xiàn)。下面的代碼是不是就很熟悉了,從請(qǐng)求頭中獲取附帶的 JWT 訪問(wèn)令牌,然后驗(yàn)證該令牌的有效性,核心代碼如下所示。
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 源碼
注冊(cè) Jwt 驗(yàn)證處理器
在 ASP.NET Core 中,你可以使用各種驗(yàn)證處理器,并不僅僅只能使用一個(gè),驗(yàn)證控制器需要一個(gè)名稱,它被看作該驗(yàn)證模式 Schema 的名稱。Jwt 驗(yàn)證模式的默認(rèn)名稱就是 "Bearer",通過(guò)字符串常量 JwtBearerDefaults.AuthenticationScheme 定義。
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 源碼
最終通過(guò) AuthenticationBuilder 的擴(kuò)展方法 AddJwtBearer() 將 Jwt 驗(yàn)證控制器注冊(cè)到依賴注入的容器中。
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 擴(kuò)展方法源碼
驗(yàn)證架構(gòu) Schema
一種驗(yàn)證處理器,加上對(duì)應(yīng)的驗(yàn)證配置選項(xiàng),我們?cè)贋樗鹨粋€(gè)名字,組合起來(lái)就成為一種驗(yàn)證架構(gòu) Schema。在 ASP.NET Core 中,可以注冊(cè)多種驗(yàn)證架構(gòu)。例如,授權(quán)策略可以使用架構(gòu)的名稱來(lái)指定所使用的驗(yàn)證架構(gòu)來(lái)使用特定的驗(yàn)證方式。在配置驗(yàn)證的時(shí)候,通常設(shè)置默認(rèn)的驗(yàn)證架構(gòu)。當(dāng)沒(méi)有指定驗(yàn)證架構(gòu)的時(shí)候,就會(huì)使用默認(rèn)架構(gòu)進(jìn)行處理。
還可以
對(duì)于 authenticate, challenge, 以及 forbid 操作使用不同的驗(yàn)證架構(gòu)
使用策略來(lái)組合多種驗(yàn)證架構(gòu)
注冊(cè)的驗(yàn)證模式,最終變成 AuthenticationScheme,注冊(cè)到依賴注入服務(wù)中。
public class AuthenticationScheme
{
public string Name { get; }
public string? DisplayName { get; }
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public Type HandlerType { get; }
}
在 GitHub 中查看 AuthenticationScheme 源碼
使用驗(yàn)證處理器
IAuthenticationSchemeProvider
各種驗(yàn)證架構(gòu)被保存到一個(gè) IAuthenticationSchemeProvider 中。
public interface IAuthenticationSchemeProvider
{
Task> GetAllSchemesAsync();
Task GetSchemeAsync(string name);
void AddScheme(AuthenticationScheme scheme);
void RemoveScheme(string name);
}
在 GitHub 中查看 IAuthenticationSchemeProvider 源碼
IAuthenticationHandlerProvider
最終的使用是通過(guò) IAuthenticationHandlerProvider 來(lái)實(shí)現(xiàn)的,通過(guò)一個(gè)驗(yàn)證模式的字符串名稱,可以取得所對(duì)應(yīng)的驗(yàn)證控制器。
public interface IAuthenticationHandlerProvider
{
Task GetHandlerAsync(HttpContext context, string authenticationScheme);
}
在 GitHub 中查看 IAuthenticationHandlerProvider 源碼
它的默認(rèn)實(shí)現(xiàn)是 AuthenticationHandlerProvider,源碼并不復(fù)雜。
public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider
{
public IAuthenticationSchemeProvider Schemes { get; }
private readonly Dictionary<string, IAuthenticationHandler> _handlerMap
= new Dictionary<string, IAuthenticationHandler>(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 源碼
Authentication 中間件 AuthenticationMiddleware
驗(yàn)證中間件的處理就沒(méi)有那么復(fù)雜了。
找到默認(rèn)的驗(yàn)證模式,使用默認(rèn)驗(yàn)證模式的名稱取得對(duì)應(yīng)的驗(yàn)證處理器,如果驗(yàn)證成功的話,把當(dāng)前請(qǐng)求用戶的主體放到當(dāng)前請(qǐng)求上下文的 User 上。
里面還有一段特別的代碼,用來(lái)找出哪些驗(yàn)證處理器實(shí)現(xiàn)了 IAuthenticationHandlerProvider,并依次調(diào)用它們,看看是否需要提取終止請(qǐng)求處理過(guò)程。
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://docs.microsoft.com/en-us/aspnet/core/security/authentication/?view=aspnetcore-5.0
【推薦】.NET Core開發(fā)實(shí)戰(zhàn)視頻課程?★★★
.NET Core實(shí)戰(zhàn)項(xiàng)目之CMS 第一章 入門篇-開篇及總體規(guī)劃
【.NET Core微服務(wù)實(shí)戰(zhàn)-統(tǒng)一身份認(rèn)證】開篇及目錄索引
Redis基本使用及百億數(shù)據(jù)量中的使用技巧分享(附視頻地址及觀看指南)
.NET Core中的一個(gè)接口多種實(shí)現(xiàn)的依賴注入與動(dòng)態(tài)選擇看這篇就夠了
10個(gè)小技巧助您寫出高性能的ASP.NET Core代碼
用abp vNext快速開發(fā)Quartz.NET定時(shí)任務(wù)管理界面
在ASP.NET Core中創(chuàng)建基于Quartz.NET托管服務(wù)輕松實(shí)現(xiàn)作業(yè)調(diào)度
現(xiàn)身說(shuō)法:實(shí)際業(yè)務(wù)出發(fā)分析百億數(shù)據(jù)量下的多表查詢優(yōu)化
