Tips:本篇已加入,.Net core 3.1 使用IdentityServer4 實現 OAuth2.0 --閱讀目錄 可點擊查看更多相關文章。
前言
接着上一篇 ID4 客戶端模式(Client Credentials Grant 的實現,我們會發現運用起來好像很方遍,但是結尾的那些思考又有多少同學能講的明白。
不知道大家還記不記得之前 IdentityServer4 介紹以及一些名詞解釋 文章最后有大致介紹 Authentication 和 Authorization 怎么區分,他們到底做了什么事情。
這篇文章我會深入刨析 Authentication 模型和原理。
回顧
我們先簡單的回顧一下之前文章中,我對 認證和授權 的理解:
我們做過這樣一個比喻,新員工第一天去公司報到,前台給員工發了門禁卡,那么發給你門禁卡之前,肯定已經從人事那邊確認了,這是我們的新同事,那么發門禁卡就相當於認證Authentication(識別身份),
我們拿着門禁卡刷開自己的辦公室,這一步動作就是 授權Authorization(驗證這個門禁卡有沒有刷自己辦公室的權限),
如果你用自己的門禁卡刷了財務辦公室,發現刷不開,這就是授權失敗(財務辦公室級別比較高一般員工的門禁卡沒有這個權限)。
這個門禁卡就相當於授權中心頒發的認證票據(.netcore 中稱這種令牌為 Authentication Ticket 認證票據)
當你離職的時候前台會回收門禁卡,你將無法進入公司(這就相當於 票據的回收 Ticket Revoker)
你們看這個前台是不是就像我們的認證服務器,公司是不是就像我們要訪問的資源。
AuthenticationMiddleware中間件
要完全理解 認證,需要深入的刨析這個認證中間件
還記得 上一篇ID4 客戶端授權模式 資源api輸出的 User.Claims 嗎?我們從他開始說起。
如果你有一些授權認證的開發經驗,其實不難從字面上看出 User.Claims其實就是 這個用戶的聲明(聲明就是 很多信息的組合 比如:姓名 ,郵箱,等等...)
我們通過 F12看到 ,這個User.Claims 的其實就是 ControllerBase 基類中的 一個屬性 (ClaimsPrincipal)User 里的子屬性, 獲取 這個用戶的 所有Claims
(微軟的模型中如何已經通過認證那么管道里就會有認證的一系列相關數據,這些數據可以讓我們知道認證當時一些情況,並且他也會成為之后授權的一些參數)
那么開始第一個知識點 我們看看這個User里到底有些什么。
用戶身份
有三個對象十分重要,ClaimsIdentity(這個是身份最終的產出結果),IIdentity(這個是最基礎的),Claim(這個是明細項)
上圖描述了 三個類型的關系,ClaimsIdentity 可以理解為最終的身份最小單元。他的屬性IsAuthenticated 取決於 是否有明確的 認證類型。
還有兩個類 IPrincipal 和 ClaimsPrincipal ,我們可以理解為 最終用戶,ClaimsPrincipal用戶可以有很多身份 ,我們看下圖描述的關系:
我們用一段代碼來總結一下我們這塊內容,大家可以試着操作一下,然后看看對象的監視器里有哪些屬性,值分別是什么。
var identity1 = new ClaimsIdentity(authenticationType: "AT1", claims: new Claim[] { new Claim(ClaimTypes.Name, "Benjamin") }); var identity2 = new ClaimsIdentity(authenticationType: "AT2", claims: new Claim[] { new Claim(ClaimTypes.Name, "Benjamin2") }); var claimPrincipal = new ClaimsPrincipal(new ClaimsIdentity[] { identity1, identity2 }); var claimPrincipal2 = new ClaimsPrincipal(identity1);
我們再 切換到之前的 ID4 實現的案例,我們看一下ID4 客戶端授權模式下 最終體現出來的用戶和身份是啥樣子的
這個用戶里面有一個identity 一對一的關系,再看一下認證的類型是 AuthenticationTypes.Federation ,並且有8個聲明
{nbf: 1601621754}
{exp: 1601625354}
{iss: http://localhost:5000}
{aud: api1}
{client_id: client}
{jti: 36D4DF4DED77F00BC6EC52914B11D33B}
{iat: 1601621754}
{scope: api1}
nbf,jti,iat都是JWT的 一些特有屬性這個之后專門為大家講解。
我們從結果已經能透徹的分析出 認證的內容了,那么認證內部是怎么處理請求的呢?我們接着往下看,微軟的認證都是基於票據的,那么還有一個知識點必不可少那就是認證票據
AuthenticationTicket
我們最終頒發出去,並且認證的時候看的都是票據,票據具體會放哪些內容,怎么加密呢,我們看一下下面這張圖
認證票據是一個帶有方案名描述的用戶身份類,並且還會帶有很多認證需要的信息,這個標簽通過TickerDataFormat進行格式化
格式化內容包括序列化和加密都是通過SecureDataFormat基類繼承,其中序列化通過 TicketSerializer完成,加密需要自定義實現IDataProtector
身份用戶也有了,認證票據也頒發出來了,那么接下來看一下一個核心處理接口:IAuthenticationHandler
認證的核心
為了更形象的表述一個請求時怎么通過驗證或者被拒絕的 有這3個類起了很關鍵的作用,源碼地址:https://github.com/aspnet/Security
1. IAuthenticationHandler
AuthenticateAsync 是認證的核心方法
ChallengeAsync 作用是返回是否 401未認證
ForbidAsync 作用返回403 無權限
InitializeAsync 完成一些初始話工作
最終處理完的認證會返回 AuthenticateResult 具體內容如下:
2.IAuthenticationHandlerProvider
認證處理的適配器(我是這么翻譯的)
我們可以自定義很多類 去實現IAuthenticationHandler接口,那么由誰去最終選擇呢,那無疑就是這個認證處理的適配器了
這個適配器就提供一個方法
我們可以根據認證的方案名 去具體講各個請求轉到不同的Handler的實現里去。
那么問題又來了,方案名我們怎么注冊進去那就要看最后一個類了 AuthenticationSchemeProvider
3.AuthenticationSchemeProvider
方案名的注冊 我們看一下這個代碼:
.AddAuthentication(options => { options.DefaultScheme = "BenBearer"; })
AuthorizationOptions的注冊后 AuthenticationSchemeProvider就有了一張完整的方案名與 AuthenticationScheme的關系,
那之后就可以通過 實現 IAuthenticationSchemeProvider的接口,
核心操作是如何被調用的
從上面的核心那段介紹可以看出 IAuthenticationHandler 的實現是最終的處理核心,那么netcore 從什么地方觸發這些核心處理呢,
那么這個類就很關鍵了,IAuthenticationService這個類的實現完成了
此塊內容源碼地址 https://github.com/aspnet/HttpAbstractions
最終AuthenticationService源碼如下:
namespace Microsoft.AspNetCore.Authentication { /// <summary> /// Implements <see cref="IAuthenticationService"/>. /// </summary> public class AuthenticationService : IAuthenticationService { /// <summary> /// Constructor. /// </summary> /// <param name="schemes">The <see cref="IAuthenticationSchemeProvider"/>.</param> /// <param name="handlers">The <see cref="IAuthenticationRequestHandler"/>.</param> /// <param name="transform">The <see cref="IClaimsTransformation"/>.</param> public AuthenticationService(IAuthenticationSchemeProvider schemes, IAuthenticationHandlerProvider handlers, IClaimsTransformation transform) { Schemes = schemes; Handlers = handlers; Transform = transform; } /// <summary> /// Used to lookup AuthenticationSchemes. /// </summary> public IAuthenticationSchemeProvider Schemes { get; } /// <summary> /// Used to resolve IAuthenticationHandler instances. /// </summary> public IAuthenticationHandlerProvider Handlers { get; } /// <summary> /// Used for claims transformation. /// </summary> public IClaimsTransformation Transform { get; } /// <summary> /// Authenticate for the specified authentication scheme. /// </summary> /// <param name="context">The <see cref="HttpContext"/>.</param> /// <param name="scheme">The name of the authentication scheme.</param> /// <returns>The result.</returns> 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); if (handler == null) { throw await CreateMissingHandlerException(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; } /// <summary> /// Challenge the specified authentication scheme. /// </summary> /// <param name="context">The <see cref="HttpContext"/>.</param> /// <param name="scheme">The name of the authentication scheme.</param> /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> /// <returns>A task.</returns> public virtual async Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties) { if (scheme == null) { var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync(); scheme = defaultChallengeScheme?.Name; if (scheme == null) { throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found."); } } var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { throw await CreateMissingHandlerException(scheme); } await handler.ChallengeAsync(properties); } /// <summary> /// Forbid the specified authentication scheme. /// </summary> /// <param name="context">The <see cref="HttpContext"/>.</param> /// <param name="scheme">The name of the authentication scheme.</param> /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> /// <returns>A task.</returns> public virtual async Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties) { if (scheme == null) { var defaultForbidScheme = await Schemes.GetDefaultForbidSchemeAsync(); scheme = defaultForbidScheme?.Name; if (scheme == null) { throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultForbidScheme found."); } } var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { throw await CreateMissingHandlerException(scheme); } await handler.ForbidAsync(properties); } /// <summary> /// Sign a principal in for the specified authentication scheme. /// </summary> /// <param name="context">The <see cref="HttpContext"/>.</param> /// <param name="scheme">The name of the authentication scheme.</param> /// <param name="principal">The <see cref="ClaimsPrincipal"/> to sign in.</param> /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> /// <returns>A task.</returns> public virtual async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (scheme == null) { var defaultScheme = await Schemes.GetDefaultSignInSchemeAsync(); scheme = defaultScheme?.Name; if (scheme == null) { throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignInScheme found."); } } var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { throw await CreateMissingSignInHandlerException(scheme); } var signInHandler = handler as IAuthenticationSignInHandler; if (signInHandler == null) { throw await CreateMismatchedSignInHandlerException(scheme, handler); } await signInHandler.SignInAsync(principal, properties); } /// <summary> /// Sign out the specified authentication scheme. /// </summary> /// <param name="context">The <see cref="HttpContext"/>.</param> /// <param name="scheme">The name of the authentication scheme.</param> /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> /// <returns>A task.</returns> public virtual async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties) { if (scheme == null) { var defaultScheme = await Schemes.GetDefaultSignOutSchemeAsync(); scheme = defaultScheme?.Name; if (scheme == null) { throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignOutScheme found."); } } var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { throw await CreateMissingSignOutHandlerException(scheme); } var signOutHandler = handler as IAuthenticationSignOutHandler; if (signOutHandler == null) { throw await CreateMismatchedSignOutHandlerException(scheme, handler); } await signOutHandler.SignOutAsync(properties); } private async Task<Exception> CreateMissingHandlerException(string scheme) { var schemes = string.Join(", ", (await Schemes.GetAllSchemesAsync()).Select(sch => sch.Name)); var footer = $" Did you forget to call AddAuthentication().Add[SomeAuthHandler](\"{scheme}\",...)?"; if (string.IsNullOrEmpty(schemes)) { return new InvalidOperationException( $"No authentication handlers are registered." + footer); } return new InvalidOperationException( $"No authentication handler is registered for the scheme '{scheme}'. The registered schemes are: {schemes}." + footer); } private async Task<string> GetAllSignInSchemeNames() { return string.Join(", ", (await Schemes.GetAllSchemesAsync()) .Where(sch => typeof(IAuthenticationSignInHandler).IsAssignableFrom(sch.HandlerType)) .Select(sch => sch.Name)); } private async Task<Exception> CreateMissingSignInHandlerException(string scheme) { var schemes = await GetAllSignInSchemeNames(); // CookieAuth is the only implementation of sign-in. var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?"; if (string.IsNullOrEmpty(schemes)) { return new InvalidOperationException( $"No sign-in authentication handlers are registered." + footer); } return new InvalidOperationException( $"No sign-in authentication handler is registered for the scheme '{scheme}'. The registered sign-in schemes are: {schemes}." + footer); } private async Task<Exception> CreateMismatchedSignInHandlerException(string scheme, IAuthenticationHandler handler) { var schemes = await GetAllSignInSchemeNames(); var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for SignInAsync. "; if (string.IsNullOrEmpty(schemes)) { // CookieAuth is the only implementation of sign-in. return new InvalidOperationException(mismatchError + $"Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and SignInAsync(\"Cookies\",...)?"); } return new InvalidOperationException(mismatchError + $"The registered sign-in schemes are: {schemes}."); } private async Task<string> GetAllSignOutSchemeNames() { return string.Join(", ", (await Schemes.GetAllSchemesAsync()) .Where(sch => typeof(IAuthenticationSignOutHandler).IsAssignableFrom(sch.HandlerType)) .Select(sch => sch.Name)); } private async Task<Exception> CreateMissingSignOutHandlerException(string scheme) { var schemes = await GetAllSignOutSchemeNames(); var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?"; if (string.IsNullOrEmpty(schemes)) { // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it. return new InvalidOperationException($"No sign-out authentication handlers are registered." + footer); } return new InvalidOperationException( $"No sign-out authentication handler is registered for the scheme '{scheme}'. The registered sign-out schemes are: {schemes}." + footer); } private async Task<Exception> CreateMismatchedSignOutHandlerException(string scheme, IAuthenticationHandler handler) { var schemes = await GetAllSignOutSchemeNames(); var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for {nameof(SignOutAsync)}. "; if (string.IsNullOrEmpty(schemes)) { // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it. return new InvalidOperationException(mismatchError + $"Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and {nameof(SignOutAsync)}(\"Cookies\",...)?"); } return new InvalidOperationException(mismatchError + $"The registered sign-out schemes are: {schemes}."); } } }
里面有我們上一段落的所有涉及到的對象會注入,
然后HttpContext的上下文中,會有很多擴展方法進行認證這塊的操作
AuthenticationHttpContextExtensions。
這塊的源碼地址:https://github.com/aspnet/HttpAbstractions/blob/master/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationHttpContextExtensions.cs
服務的注冊
最終服務的注冊中會把 IAuthenticaitonServer的實現 AuthenticaitonServer ,IAuthenticationHandlerProvider的實現,IAuthenticationSchemeProvider的實現注入進去。
最后 認證的中間件AuthenticationMiddleware所作的是事情就是當請求路過這個中間件的時候,攔截處理返回,並將一些信息附加到HttpContext上。
是不是感覺也沒有那么復雜那么難。
源碼分析不是很多,但是Authencation認證這塊基本流程就是基本上述這么一些,分析可能不是很到位,但是希望可以給大家一些啟發。
如果覺得本篇隨筆對你有幫助,請點擊右下方的 【推薦👍】,或者給作者 【打賞】,感謝大家的支持,這將成為作者繼續寫作的動力 |