本文主要參考以下系列文章
https://www.cnblogs.com/RainingNight/tag/Authentication/
https://www.cnblogs.com/jionsoft/tag/身份驗證/
前言
在 ASP.NET Core沿用了ASP.NET里面的Identity組件庫,負責對用戶的身份進行認證。ASP.NET Core提倡的是基於聲明(Claim)的認證。
基於聲明的認證對認證和授權進行了明確的區分,認證用來頒發一個用戶的身份標識。而授權則是通過獲取身份標識中的信息,來判斷該用戶能做什么,不能做什么。
Claims相關對象
- Claim:證件單元,存儲信息最小單位
- ClaimsIdentity:相當於是一個證件
- ClaimsPrincipal:則是證件的持有者
一個ClaimsPrincipal中可以有多個ClaimsIdentity,而一個ClaimsIdentity中可以有多個Claim。
Claim:證件單元
Claim claim = new Claim(ClaimTypes.NameIdentifier, user.Code);
ClaimsIdentity:證件
public class ClaimsIdentity:IIdentity
{
public ClaimsIdentity(IEnumerable<Claim> claims){}
public virtual string Name { get; }
public string Label { get; set; }
//證件類型
public virtual string AuthenticationType { get; }
//是否是合法的證件。
bool IsAuthenticated { get; }
public virtual void AddClaim(Claim claim);
public virtual void RemoveClaim(Claim claim);
public virtual void FindClaim(Claim claim);
}
ClaimsPrincipal:證件當事人
現實生活中,一個人有多種證件,比如身份證、登機牌、就診卡等,你去到不同的機構需要出示不同的證件,ClaimsPrincipal就代表上面說的這個人,證件用ClaimsIdentity表示,當然得有一張證件作為主要證件(如身份證)。上面我們一直拿一個人擁有多張證件來舉例,其實並不准確,因為對系統來說並不關心是誰登錄,可能是一個用戶、也可能是一個第三方應用。所以將ClaimsPrincipal理解為一個登錄到系統的主體更合理。
在一個系統中可能同時存在多種身份驗證方案,比如我們系統本身做了用戶管理功能,使用最簡單的cookie身份驗證方案,或者使用第三方登錄,微信、QQ、支付寶賬號登錄,通常一個身份驗證方案可以產生一張證件(ClaimsIdentity),當然某個身份驗證方案也可以將獲得的Claim添加到一張現有的證件中,這個是靈活的。默認情況下,用戶登錄時asp.net core會選擇設置好的默認身份驗證方案做身份驗證,本質是創建一個ClaimsPrincipal,並根據當前請求創建一個證件(ClaimsIdentity),然后將此ClaimsIdentity加入到ClaimsPrincipal,最后將這個ClaimsPrincipal設置到HttpContext.User屬性上。
當用戶登錄后,我們已經可以從HttpContext.User拿到當前用戶,里面就包含一張或多張證件,后續的權限判斷通常就依賴里面的信息,比如所屬角色、所屬部門,除了證件的信息我們也可以通過用戶id去數據庫中查詢得到更多用戶信息作為權限判斷的依據。
public class ClaimsPrincipal:IPrincipal
{
public ClaimsPrincipal(IEnumerable<ClaimsIdentity> identities){}
//當事人的主身份證件
public virtual IIdentity Identity { get; }
public virtual IEnumerable<ClaimsIdentity> Identities { get; }
//在否屬於某個角色
bool IsInRole(string role);
public virtual void AddIdentity(ClaimsIdentity identity);
}
AuthenticationHandler(身份驗證處理器)
AuthenticationHandler中包含身份驗證流程中核心的處理步驟,下面以cookie身份驗證為例:
- SignIn:在登錄時驗證通過后將用戶標識加密后存儲到cookie
- SignOut:當用戶注銷時,需要清楚代表用戶標識的cookie
- Authenticate:在登錄時從請求中獲取用戶標識
- Challenge:質詢/挑戰,意思是當發現沒有從當前請求中發現用戶標識時怎么辦,可能是跳轉到登錄頁,也可能是直接響應401,或者跳轉到第三方(如QQ、微信)的登錄頁
- Forbid:如權限驗證不通過
身份驗證處理器就是用來跟身份驗證相關的步驟的,這些步驟在系統的不同地方來調用(比如在登錄頁對應的Action、在請求抵達時、在授權中間件中),
每個調用時都可以指定使用哪種身份驗證方案,如果不提供將使用默認方案來做對應的操作。
不同的身份驗證方式有不同的AuthenticationHandler實現
IAuthenticationHandler
IAuthenticationHandler接口只定義了最核心的幾個步驟:Authenticate()、Challenge()、Forbid()。登錄和注銷這兩個步驟定義了對應的子接口。
AuthenticationSchemeOptions(某個具體的身份驗證方案的選項)
在上述身份驗證處理的多個步驟中會用到一些選項數據,比如基於cookie的身份驗證 cookeName、有效時長、再比如從請求時從cookie中解析得到用戶標識后回調選項中的某個回調函數,允許我們的代碼向調試中添加額外數據,或者干脆替換整個標識。
所以身份驗證選項用來允許我們控制AuthenticationHandler的執行。不同的身份驗證方式有不同的選項對象,比如:CookieAuthenticationOptions、JwtBearerOptions.. 它們直接或間接繼承AuthenticationSchemeOptions
我們以cookie認證舉例
CookieAuthenticationOptions就是針對這個cookie身份驗證方案的選項對象
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie("換個方案名","顯示名",opt=> {
opt.SlidingExpiration = true;
//其它設置...
}).AddJwtBearer();
AuthenticationOptions(整個應用的身份驗證選項)
上面說的AuthenticationSchemeOptions是指某個具體身份驗證方案的選項。AuthenticationOptions則是針對整個身份驗證功能的選項對象,我們需要在應用啟動階段通過它來配置身份驗證功能。
AuthenticationOptions對象允許我們對身份驗證做整體配置,這個配置主要體現為:支持的身份驗證方案列表;指定默認身份驗證方案、默認登錄時用的身份驗證方案...默認注銷...等。我們可以通過AddAuthentication(Action
services.AddAuthentication(authenticationOptions=> {
authenticationOptions.AddScheme<CookieAuthenticationHandler>("cookie", "顯示名cookie");
authenticationOptions.AddScheme<JwtBearerHandler>("jwt","顯示名jwtToken");
authenticationOptions.DefaultAuthenticateScheme = "cookie";
//...其它配置
});
AddScheme添加身份驗證方案。
public void AddScheme(string name, Action<AuthenticationSchemeBuilder> configureBuilder)
{
var builder = new AuthenticationSchemeBuilder(name);
configureBuilder(builder);
_schemes.Add(builder);
}
name方案名;configureBuilder允許我們提供委托對方案進行配置
添加的這些方案最終會被存儲到AuthenticationSchemeProvider供其使用
另外DefaultAuthenticateScheme、DefaultSignInScheme、DefaultSignOutScheme..看名字也曉得它是說當我們調用某個步驟未指定使用那個方案是的默認選擇
AuthenticationScheme(身份驗證方案)
總結性的說:身份驗證方案 = 名稱 + 身份驗證處理器類型 + 選項,暫時可以理解一種身份驗證方式 對應 一個身份驗證方案,比如:
基於用戶名密碼+cookie的身份驗證方式 對應的 身份驗證方案為:new AuthenticationScheme("UIDPWDCookie",typeof(CookieAuthenticationHandler))
基於用戶名密碼+token 的身份驗證方式 對應的 身份驗證方案為:new AuthenticationScheme("JwtBearer",typeof(JwtBearerHandler))
身份驗證方案在程序啟動階段配置,啟動后形成一個身份驗證方案列表。
程序運行階段從這個列表中取出指定方案,得到對應的處理器類型,然后創建它,最后調用這個處理器做相應處理
比如登錄操作的Action中xxx.SignIn("方案名") > 通過方案名找到方案從而得到對應的處理器類型 > 創建處理器 > 調用其SignIn方法
一種特殊的情況可能多種方案使用同一個身份驗證處理器類型,這個后續的集成第三方登錄來說
身份驗證方案的容器AuthenticationSchemeProvider
它是身份驗證方案的容器(Dictionary<方案名,身份驗證方案>),默認是單例形式注冊到依賴注入容器的
在應用啟動時通過AuthenticationOptions添加的各種身份驗證方案會被存儲到這個容器中
各種GetDefaultXXX用來獲取針對特定步驟的默認方案,如:GetDefaultAuthenticateSchemeAsync中間件從請求獲取用戶標識時用來獲取針對此步驟的默認方案、GetDefaultSignInSchemeAsync獲取默認用來登錄的方案...等等,身份驗證的不同步驟可以設置不同的默認方案。如果針對單獨的步驟沒有設置默認方案,則自動嘗試獲取默認方案,通過AuthenticationOptions設置這些默認值
身份驗證過程中各個步驟都會通過AuthenticationSchemeProvider對象拿到指定方案,並通過關聯的身份驗證類型獲得最終身份驗證處理器,然后做相應處理
由於AuthenticationSchemeProvider的構造函數定義了IOptions
public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider
{
public AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options)
: this(options, new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal))
{
}
protected AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes)
{
_options = options.Value;
_schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
_requestHandlers = new List<AuthenticationScheme>();
foreach (var builder in _options.Schemes)
{
var scheme = builder.Build();
//options中的scheme添加到_schemes
AddScheme(scheme);
}
}
private readonly AuthenticationOptions _options;
private readonly object _lock = new object();
//已注冊的scheme
private readonly IDictionary<string, AuthenticationScheme> _schemes;
//實現了IAuthenticationRequestHandler接口的Scheme
private readonly List<AuthenticationScheme> _requestHandlers;
//用作枚舉api的安全返回值(防止遍歷的時候刪除元素拋出異常)
private IEnumerable<AuthenticationScheme> _schemesCopy = Array.Empty<AuthenticationScheme>();
private IEnumerable<AuthenticationScheme> _requestHandlersCopy = Array.Empty<AuthenticationScheme>();
private Task<AuthenticationScheme?> GetDefaultSchemeAsync()
=> _options.DefaultScheme != null
? GetSchemeAsync(_options.DefaultScheme)
: Task.FromResult<AuthenticationScheme?>(null);
public virtual Task<AuthenticationScheme?> GetDefaultAuthenticateSchemeAsync()
=> _options.DefaultAuthenticateScheme != null
? GetSchemeAsync(_options.DefaultAuthenticateScheme)
: GetDefaultSchemeAsync();
public virtual Task<AuthenticationScheme?> GetDefaultChallengeSchemeAsync()
=> _options.DefaultChallengeScheme != null
? GetSchemeAsync(_options.DefaultChallengeScheme)
: GetDefaultSchemeAsync();
public virtual Task<AuthenticationScheme?> GetDefaultForbidSchemeAsync()
=> _options.DefaultForbidScheme != null
? GetSchemeAsync(_options.DefaultForbidScheme)
: GetDefaultChallengeSchemeAsync();
public virtual Task<AuthenticationScheme?> GetDefaultSignInSchemeAsync()
=> _options.DefaultSignInScheme != null
? GetSchemeAsync(_options.DefaultSignInScheme)
: GetDefaultSchemeAsync();
public virtual Task<AuthenticationScheme?> GetDefaultSignOutSchemeAsync()
=> _options.DefaultSignOutScheme != null
? GetSchemeAsync(_options.DefaultSignOutScheme)
: GetDefaultSignInSchemeAsync();
public virtual Task<AuthenticationScheme?> GetSchemeAsync(string name)
=> Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null);
public virtual Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync()
=> Task.FromResult(_requestHandlersCopy);
public virtual bool TryAddScheme(AuthenticationScheme scheme)
{
if (_schemes.ContainsKey(scheme.Name))
{
return false;
}
lock (_lock)
{
if (_schemes.ContainsKey(scheme.Name))
{
return false;
}
if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))
{
_requestHandlers.Add(scheme);
_requestHandlersCopy = _requestHandlers.ToArray();
}
_schemes[scheme.Name] = scheme;
_schemesCopy = _schemes.Values.ToArray();
return true;
}
}
public virtual void AddScheme(AuthenticationScheme scheme)
{
if (_schemes.ContainsKey(scheme.Name))
{
throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
}
lock (_lock)
{
if (!TryAddScheme(scheme))
{
throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
}
}
}
public virtual void RemoveScheme(string name)
{
if (!_schemes.ContainsKey(name))
{
return;
}
lock (_lock)
{
if (_schemes.ContainsKey(name))
{
var scheme = _schemes[name];
if (_requestHandlers.Remove(scheme))
{
_requestHandlersCopy = _requestHandlers.ToArray();
}
_schemes.Remove(name);
_schemesCopy = _schemes.Values.ToArray();
}
}
}
public virtual Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync()
=> Task.FromResult(_schemesCopy);
}
AuthenticationHandlerProvider(身份驗證處理器工廠)
可以把它理解為AuthenticationHandler的運行時容器或工廠
它是以Scope的形式注冊到依賴注入容器的,所以每次請求都會創建一個實例對象。
唯一方法GetHandlerAsync從AuthenticationSchemeProvider獲取指定身份驗證方案,然后通過方案關聯的AuthenticationHandler Type從依賴注入容器中獲取AuthenticationHandler ,獲取的AuthenticationHandler會被緩存,這樣同一個請求的后續調用直接從緩存中拿。
AuthenticationService就是通過它來得到AuthenticationHandler然后完成身份驗證各種功能的
public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider
{
public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes)
{
Schemes = schemes;
}
public IAuthenticationSchemeProvider Schemes { get; }
// handler緩存
private readonly Dictionary<string, IAuthenticationHandler> _handlerMap = new Dictionary<string, IAuthenticationHandler>(StringComparer.Ordinal);
public async Task<IAuthenticationHandler?> 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;
}
//ActivatorUtilities定義在Microsoft.Extensions.DependencyInjection.Abstractions中
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;
}
}
身份驗證服務AuthenticationService
身份驗證中的步驟是在多個地方被調用的,身份驗證中間件、授權中間件、登錄的Action(如:AccountController.SignIn())、注銷的Action(如:AccountController.SignOut()),身份驗證的核心方法定義在這個類中,但它本質上還是去找到對應的身份驗證處理器並調用其同名方法。其實這些方法還進一步以擴展方法的形式定義到HttpContext上了。以SignIn方法為例
HttpContext.SignIn() > AuthenticationService.SignIn() > AuthenticationHandler.SignIn()
IAuthenticationService
IAuthenticationService是用來對外提供一個統一的認證服務接口
public interface IAuthenticationService
{
Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme);
Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties);
Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties);
Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties);
Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties);
}
IAuthenticationService 的默認實現 AuthenticationService 中的邏輯就非常簡單了,只是調用Handler中的同名方法:
public class AuthenticationService : IAuthenticationService
{
public IAuthenticationSchemeProvider Schemes { get; }
public IAuthenticationHandlerProvider Handlers { get; }
public IClaimsTransformation Transform { get; }
public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
{
if (scheme == null)
{
var defaultScheme = await Schemes.GetDefaultAuthenticateSchemeAsync();
scheme = defaultScheme?.Name;
if (scheme == null)
{
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found.");
}
}
var handler = await Handlers.GetHandlerAsync(context, scheme);
var result = await handler.AuthenticateAsync();
if (result != null && result.Succeeded)
{
var transformed = await Transform.TransformAsync(result.Principal);
return AuthenticateResult.Success(new AuthenticationTicket(transformed, result.Properties, result.Ticket.AuthenticationScheme));
}
return result;
}
}
AuthenticationResult
AuthenticateResult 用來表示認證的結果
public class AuthenticateResult
{
public AuthenticationTicket Ticket { get; protected set; }
public bool Succeeded => Ticket != null;
public ClaimsPrincipal Principal => Ticket?.Principal;
public AuthenticationProperties Properties => Ticket?.Properties;
public Exception Failure { get; protected set; }
public bool None { get; protected set; }
public static AuthenticateResult Success(AuthenticationTicket ticket) => new AuthenticateResult() { Ticket = ticket };
public static AuthenticateResult NoResult() => new AuthenticateResult() { None = true };
public static AuthenticateResult Fail(Exception failure) => new AuthenticateResult() { Failure = failure };
public static AuthenticateResult Fail(string failureMessage) => new AuthenticateResult() { Failure = new Exception(failureMessage) };
}
AuthenticationHttpContextExtensions
AuthenticationHttpContextExtensions 類是對 HttpContext 認證相關的擴展,在Core2.0中是使用HttpContext的Authentication屬性做認證
public static class AuthenticationHttpContextExtensions
{
//驗證在 SignInAsync 中頒發的證書,並返回一個 AuthenticateResult 對象,表示用戶的身份。
public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) =>
context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme);
//返回一個需要認證的標識來提示用戶登錄,通常會返回一個 401 狀態碼
public static Task ChallengeAsync(this HttpContext context, string scheme, AuthenticationProperties properties) { }
//禁止訪問,表示用戶權限不足,通常會返回一個 403 狀態碼
public static Task ForbidAsync(this HttpContext context, string scheme, AuthenticationProperties properties) { }
//用戶登錄成功后頒發一個證書(加密的用戶憑證),用來標識用戶的身份
public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) {}
//退出登錄,如清除Coookie等
public static Task SignOutAsync(this HttpContext context, string scheme, AuthenticationProperties properties) { }
//用來獲取 AuthenticationProperties 中保存的額外信息
public static Task<string> GetTokenAsync(this HttpContext context, string scheme, string tokenName) { }
}
以上方法都是通過從DI系統中獲取到 IAuthenticationService 接口實例,然后調用其同名方法
public static IServiceCollection AddAuthenticationCore(this IServiceCollection services)
{
services.TryAddScoped<IAuthenticationService, AuthenticationService>();
services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>(); // Can be replaced with scoped ones that use DbContext
services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>();
services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>();
return services;
}