話題背景
關於認證我的個人理解是,驗證信息的合法性。在我們生活當中,比如門禁,你想進入一個有相對安全措施的小區或者大樓,你需要向保安或者門禁系統提供你的身份信息證明,只有確定你是小區業主,才可以進來,我這只是打個比方啊,不要糾結。對於我們計算機的安全領域,認證其實也非常類似,windows系統登陸就是一個很好的例子。今天我們主要學習的是ASPNET以及ASPNETCORE平台上面一些主流的認證方式。
正式話題-認證
我最開始接觸NET平台的WEB框架是從APSNETWEBFORM開始->ASPNETMVC->ASPNETMVCCORE,下面我們就從WEBFORM開始吧(包括MVC1.x-4.x)。在MVC5之前,我們常用的認證方式有,Forms、Windows、Passport、None這三種認證方式,嚴格意義上說是三種,None為不認證,而在這三種認證方式當中,我們最常用的就是Forms表單認證,下面我們一起來看看Forms表單認證的實現原理。
Forms表單認證
我會以我自己的使用方式介紹再到實現原理。整個Forms認證的實現邏輯大概是,說到Forms認證我們就不得不說ASPNET處理管道,為什么這么說呢?因為ASPNET的很多基礎功能都是通過相應的HttpModule實現的,比如認證、授權、緩存、Session等等。ASPNET平台的Forms認證就是基於FormsAuthenticationModule模塊實現,相應的Windows認證也是一樣,由WindowsAuthenticationModule實現。對於Forms認證方式登錄而言。
1.匹配用戶名&密碼是否正確。
2.構建FormsAuthenticationTicket對象。
3.通過FormsAuthentication.Encrypt方法加密Ticker信息。
4.基於加密Ticker信息,構建HttpCookie對象。
5.寫入Response,輸出到客戶端。
以上就是我們基於Forms表單認證方式的登錄實現邏輯,下面我們來梳理一下認證的大概實現邏輯,針對每次請求而言。
1.在ASPNET管道生命周期里,認證模塊FormsAuthenticationModule會接管並讀取Cookie。
2.解密Cookie獲取FormsAuthenticationTicket對象並且驗證是否過期。
3.根據FormsAuthenticationTicket對象構造FormsIdentity對象並設置HttpContext.User。
4.完成認證。
下面我們一起看看Forms認證的具體實現,我會以我自己開發過程中使用的方式加以介紹。首先我們會在web.config文件里面定義authentication配置節點,如下。
1 <authentication mode="Forms"> 2 <forms name="AUTH" loginUrl="~/login" protection="All" timeout="43200" path="/" requireSSL="false" slidingExpiration="true" /> 3 </authentication>
mode屬性對應了4屬性值,除Forms以外還有上面我提到的三種方式。其他三種由於篇幅問題,在這里不做介紹。這些屬性我相信大家應該都比較熟悉。下面我們看看關於Forms認證具體的后台代碼。看代碼。
1 public virtual void SignIn(User user, // 這個user是你校驗合法性之后的這么一個用戶標識對象 2 bool createPersistentCookie) 3 { 4 var now = DateTime.UtcNow.ToLocalTime(); 5 // 構建Ticker對象 6 var ticket = new FormsAuthenticationTicket( 7 1 , 8 user.Username, 9 now, 10 now.Add(_expirationTimeSpan), 11 createPersistentCookie, 12 user.Username, 13 FormsAuthentication.FormsCookiePath); 14 // 加密ticker對象 15 var encryptedTicket = FormsAuthentication.Encrypt(ticket); 16 // 通過加密ticker對象構建HttpCookie對象 17 var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket); 18 cookie.HttpOnly = true; 19 if (ticket.IsPersistent) 20 { 21 cookie.Expires = ticket.Expiration; 22 } 23 cookie.Secure = FormsAuthentication.RequireSSL; 24 cookie.Path = FormsAuthentication.FormsCookiePath; 25 if (FormsAuthentication.CookieDomain != null) 26 { 27 cookie.Domain = FormsAuthentication.CookieDomain; 28 } 29 // 寫入輸出流Response 30 _httpContext.Response.Cookies.Add(cookie); 31 }
以上代碼就完成了我們的Forms認證所需的Cookie信息,可能有些朋友在以往開發WebForms到4.x最常用的使用方式是FormsAuthentication.SetAuthCookie(user.UserName, true),其實SetAuthCookie里面的實現邏輯跟上面實現大同小異,只是我比較喜歡手動創建可以更多的控制一些輔助信息而已。在以上代碼片段中,我着重想介紹一下FormsAuthentication.Encrypt(ticket)加密方法,因為它涉及到了Forms認證的安全機制,也好讓各位朋友大概了解Forms認證到底安全不安全。FormsAuthentication該對象位於System.Web.Security名稱空間下面,主要作用是安全相關輔助工具類,比如加解密等。
1.在默認情況下,ASPNETFORMS認證模塊針對Ticker的加密Key是由ASPNET隨機生成,並存儲在本地安全機構LSA中。我們可以通過一下代碼片段驗證這一邏輯。
1 private CryptographicKey GenerateCryptographicKey(string configAttributeName, string configAttributeValue, int autogenKeyOffset, int autogenKeyCount, string errorResourceString) 2 { 3 // 其他代碼 4 bool flag1 = false; 5 bool flag2 = false; 6 bool flag3 = false; 7 if (configAttributeValue != null) 8 { 9 string str1 = configAttributeValue; 10 char[] chArray = new char[1]{ ',' }; 11 foreach (string str2 in str1.Split(chArray)) 12 { 13 if (!(str2 == "AutoGenerate")) 14 { 15 if (!(str2 == "IsolateApps")) 16 { 17 if (!(str2 == "IsolateByAppId")) 18 flag3 = true; 19 } 20 else 21 flag2 = true; 22 } 23 else 24 flag1 = true; 25 } 26 } 27 if (flag2) 28 MachineKeyMasterKeyProvider.AddSpecificPurposeString((IList<string>) stringList, "IsolateApps", this.ApplicationName); 29 if (flag3) 30 MachineKeyMasterKeyProvider.AddSpecificPurposeString((IList<string>) stringList, "IsolateByAppId", this.ApplicationId); 31 }
以上代碼片段邏輯也比較簡單,自己體會吧。
2.手動指定machineKey配置節點,該配置節在web.config文件里面,其中包括可支持的加密算法,加密算法支持DES,3DES,AES等。具體代碼我就不貼了,我們跟蹤其實現原理意在了解Forms認證其安全性。
3.通過以上兩點介紹,我個人認為Forms認證相對來說很安全。
Forms認證
下面我們看看Forms的實現原理。
ASPNET的Forms認證發生在ASPNET管道的FormsAuthenticationModule對象里面,在該對象里面的Init方法里面綁定了認證事件OnEnter,具體的認證實現是OnEnter里面調用的OnAuthenticate方法。我們來看下代碼。
1 private void OnAuthenticate(FormsAuthenticationEventArgs e) 2 { 3 // 其他代碼 4 bool cookielessTicket = false; 5 // 從請求cookie里面抽取ticker票據信息 6 FormsAuthenticationTicket ticketFromCookie = FormsAuthenticationModule.ExtractTicketFromCookie(e.Context, FormsAuthentication.FormsCookieName, out cookielessTicket); 7 // 過期或者為null直接返回 8 if (ticketFromCookie == null || ticketFromCookie.Expired) 9 return; 10 FormsAuthenticationTicket ticket = ticketFromCookie; 11 // 如果啟用滑動過期,更新過期時間 12 if (FormsAuthentication.SlidingExpiration) 13 ticket = FormsAuthentication.RenewTicketIfOld(ticketFromCookie); 14 e.Context.SetPrincipalNoDemand((IPrincipal) new GenericPrincipal((IIdentity) new FormsIdentity(ticket), new string[0])); 15 if (!cookielessTicket && !ticket.CookiePath.Equals("/")) 16 { 17 cookie = e.Context.Request.Cookies[FormsAuthentication.FormsCookieName]; 18 if (cookie != null) 19 cookie.Path = ticket.CookiePath; 20 } 21 if (ticket == ticketFromCookie) 22 return; 23 if (cookielessTicket && ticket.CookiePath != "/" && ticket.CookiePath.Length > 1) 24 ticket = FormsAuthenticationTicket.FromUtc(ticket.Version, ticket.Name, ticket.IssueDateUtc, ticket.ExpirationUtc, ticket.IsPersistent, ticket.UserData, "/"); 25 string cookieValue = FormsAuthentication.Encrypt(ticket, !cookielessTicket); 26 27 if (cookielessTicket) 28 { 29 e.Context.CookielessHelper.SetCookieValue('F', cookieValue); 30 e.Context.Response.Redirect(e.Context.Request.RawUrl); 31 } 32 else 33 { 34 if (cookie != null) 35 cookie = e.Context.Request.Cookies[FormsAuthentication.FormsCookieName]; 36 if (cookie == null) 37 { 38 cookie = new HttpCookie(FormsAuthentication.FormsCookieName, cookieValue); 39 cookie.Path = ticket.CookiePath; 40 } 41 if (ticket.IsPersistent) 42 cookie.Expires = ticket.Expiration; 43 cookie.Value = cookieValue; 44 cookie.Secure = FormsAuthentication.RequireSSL; 45 cookie.HttpOnly = true; 46 if (FormsAuthentication.CookieDomain != null) 47 cookie.Domain = FormsAuthentication.CookieDomain; 48 cookie.SameSite = FormsAuthentication.CookieSameSite; 49 e.Context.Response.Cookies.Remove(cookie.Name); 50 e.Context.Response.Cookies.Add(cookie); 51 } 52 }
以上代碼片段反映了Forms認證具體邏輯,邏輯比較簡單,我也大概做了一些注釋,以上就是ASPNET在MVC5.x之前ASPNETForms認證的實現。接下來我們對ASPNET5.X之前的版本基於Forms認證做個簡單的總結。
1.用戶在未登錄的情況下,訪問我們受保護的資源。
2.FormsAuthenticationModule模塊驗證用戶的合法性,主要是生成Identity對象和設置IsAuthenticated屬性。
3.如果未登錄則endrequest階段跳轉到web.config配置的登錄頁或者硬編碼指定的登錄頁。
4.用戶登錄。
5.匹配用戶名&密碼,如果合法,生成ticker票據和cookie並寫入response。
6.訪問受保護的資源(授權部分)。
7.FormsAuthenticationModule模塊驗證用戶的合法性。
8.如果為以認證用戶IsAuthenticated=true,授權訪問相應的資源。
后續的每次請求也是6,7,8循環。
針對Forms認證就此告一段落,下面我們接着介紹MVC5的常規認證方式。
MVC5Cookies認證方式
為什么我要把MVC5的認證方式單獨做一個小結講解呢?它有什么特別之處嗎?沒錯,ASPNETMVC5引入了新的設計理念OWin,我個人的理解是,解耦webserver容器IIS和模塊化。同時NET4.5也引入了ASPNET.Identity,Identity主要是提供幫助我們管理用戶、角色以及存儲,當然Identity相較Membership強大多了。對於OWin和Identity我在這里不做詳細介紹,自己可以去搜一些帖子看或者查看官方文檔。OWin在WebServers與ASPNETWebApplication之間定義了一套標准接口,其官方的開源實現是Katana這個開源項目,我們今天要介紹的MVC5的認證就是基於Katana這個開源項目的CookieAuthenticationMiddleware中間件實現的,在介紹CookieAuthenticationMiddleware中間件之前,我想簡單羅列一下MVC5的cookies認證(你也可以認為是Katana實現的新的Forms認證)和我們早期使用的Forms認證做個簡單的對比。
相同點:1.基於cookie認證 2.支持滑動過期策略 3.實現令牌保護 4.重定向。
不同點:Identity結合Owin實現了聲明認證Claims-based。
以上是個人的一點理解,下面我們具體看看認證中間件的實現,CookieAuthenticationMiddleware的定義。
1 public class CookieAuthenticationMiddleware : AuthenticationMiddleware<CookieAuthenticationOptions> 2 { 3 // 其他成員 4 public CookieAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, CookieAuthenticationOptions options) 5 : base(next, options) 6 { 7 } 8 // 創建具體的AuthenticationHandler 9 protected override AuthenticationHandler<CookieAuthenticationOptions> CreateHandler() 10 { 11 return new CookieAuthenticationHandler(_logger); 12 } 13 }
CookieAuthenticationMiddleware里面就一個方法成員,通過CreateHandler方法創建了具體的CookieAuthenticationHandler對象,我們的認證核心實現就發生在這個Handler里面。接下來我們看看CookieAuthenticationHandler對象的定義。
1 internal class CookieAuthenticationHandler : AuthenticationHandler<CookieAuthenticationOptions> 2 { 3 // 其他成員 4 private const string HeaderNameCacheControl = "Cache-Control"; 5 private const string HeaderNamePragma = "Pragma"; 6 private const string HeaderNameExpires = "Expires"; 7 private const string HeaderValueNoCache = "no-cache"; 8 private const string HeaderValueMinusOne = "-1"; 9 private const string SessionIdClaim = "Microsoft.Owin.Security.Cookies-SessionId"; 10 11 private bool _shouldRenew; 12 private DateTimeOffset _renewIssuedUtc; 13 private DateTimeOffset _renewExpiresUtc; 14 private string _sessionKey; 15 16 protected override async Task<AuthenticationTicket> AuthenticateCoreAsync() 17 18 protected override async Task ApplyResponseGrantAsync() 19 20 protected override Task ApplyResponseChallengeAsync() 21 }
從CookieAuthenticationHandler對象的定義來看,其實也能看出一二,主要是針對cookie的相關操作,在該對象成員里面我們需要了解一下其中的三個方法。
1.AuthenticateCoreAsync,代碼我就不貼了,有興趣的朋友可以自己查看Katana開源項目的源代碼。該方法內部大概實現思路是:從IOWinContext對象獲取cookie,如果對owin不怎么熟悉的話,這個context你可以把它理解為我們之前熟悉的HttpContext,然后通過解密出來的cookie字符串構造ClaimsIdentity對象並添加到OwinContext對象Request.User,最后返回AuthenticationTicket對象,該對象包裝的就是當前用戶信息以及相關輔助信息。
2.ApplyResponseGrantAsync,設置、更新或者刪除cookie並寫入response。
3.ApplyResponseChallengeAsync,授權失敗,發生重定向。
1 public class AuthenticationTicket 2 { 3 public AuthenticationTicket(ClaimsIdentity identity, AuthenticationProperties properties) 4 { 5 Identity = identity; 6 Properties = properties ?? new AuthenticationProperties(); 7 } 8 // 用戶信息 9 public ClaimsIdentity Identity { get; private set; } 10 // 輔助信息,比如會話、過期等 11 public AuthenticationProperties Properties { get; private set; } 12 }
下面我們一起看看在我們開發過程中的應用以及內部實現
Startup是Katana開源項目引入的一種新的模塊初始化方式,其實也沒什么特別的,就是相關中間件的注冊以及一些默認上下文對象的初始化操作。下面我們具體看代碼,我們的MVC5新的認證方式在Startup里面如何注冊的。
1 public partial class Startup 2 { 3 public void ConfigureAuth(IAppBuilder app) 4 { 5 // 其他代碼 6 app.UseCookieAuthentication(new CookieAuthenticationOptions 7 { 8 AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, 9 LoginPath = new PathString("/Account/Login"), 10 Provider = new CookieAuthenticationProvider 11 { } 12 }); 13 } 14 }
注冊邏輯很簡單,通過IAppBuilder的擴展方法UseCookieAuthentication實現,接下來我們看看UseCookieAuthentication擴展方法的內部實現。
1 public static IAppBuilder UseCookieAuthentication(this IAppBuilder app, CookieAuthenticationOptions options, PipelineStage stage) 2 { 3 if (app == null) 4 { } 5 // 注冊 6 app.Use(typeof(CookieAuthenticationMiddleware), app, options); 7 // 加入owin管道 8 app.UseStageMarker(stage); 9 return app; 10 }
整個注冊邏輯就這么幾行代碼,相關方法都有注釋。最后在程序初始化過程中通過Build方法完成Owin管道所有中間件的初始化工作。接下來我們看看具體的登錄實現。
1 public async Task<ActionResult> Login(LoginModel model,string returnUrl) 2 { 3 // 其他代碼 4 if (ModelState.IsValid) 5 { 6 AppUser user = await UserManager.FindAsync(model.Name, model.Password); 7 if (user==null) 8 { 9 ModelState.AddModelError("","無效的用戶名或密碼"); 10 } 11 else 12 { 13 var claimsIdentity = 14 await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); 15 AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, claimsIdentity); 16 return Redirect(returnUrl); 17 } 18 } 19 20 return View(model); 21 }
通過以上代碼片段就完成了我們系統的登錄操作,在以上Login方法里面,我們需要注意這么幾個方法。
1.FindAsync主要是通過Identity實現用戶名和密碼的匹配。
2.CreateIdentityAsync主要是創建ClaimsIdentity對象,該對象后續會寫入cookie。
3.SignIn包裝CreateIdentity方法創建的ClaimsIdentity以及ClaimsPrincipal對象,為cookie寫入Response提供相關認證信息,只有在設置cookie階段才會寫入response。
接下來我們針對Katana里面的cookie認證做個簡單的總結。
1.用戶在未登錄的情況下,訪問我們受保護的資源。
2.CookieAuthenticationMiddleware中間件驗證用戶的合法性。
3.用戶登錄。
4.匹配用戶名&密碼,如果合法,包裝相關認證信息。
5.創建\更新cookie寫入response。
6.訪問受保護的資源。
7.CookieAuthenticationMiddleware中間件解密cookie驗證用戶認證信息。
8.如果為以認證用戶,授權訪問相應的資源。
后續的每次請求也是6,7,8循環。
以上MVC5新的Cookies認證方式就此告一段落,下面我們接着介紹ASPNET.Identity三方認證。
三方認證
在我們介紹三方認證之前,我們不妨先來了解一下什么是Claim,大家把它翻譯成聲明,我也就這么跟着叫把。Claim所描述的是一個用戶單一的某個信息,比如用戶名,只有多個Claim組合才能描述一個完整的用戶ClaimsIdentity對象。個人理解這是一種通用的信息存儲結構,一種規范,可以很方便的基於用戶數據信息驅動認證和授權並且提供獨立服務,各自都不需要關心自己的實現。在我們傳統的認證windows或者forms認證方式中,每個系統都有自己認證方式、授權和用戶數據信息,如果是幾年以前,可能沒有什么問題,但是在如今飛速發展的互聯網時代,就顯的有很大的局限性、擴展性以及安全性。接下來我們要介紹的就是MVC5基於ASPNET.Identity結合Katana實現的三方認證,也就是我們上面說的基於Claims-based實現第三方認證,這里我們以google認證為例,由於網絡問題這里我們以木宛城主大拿的實例代碼做示例,我會結合實例代碼分析內部實現。
首先我們需要添加google服務認證中間件。
1 app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions() 2 { 3 // 以下為客戶端憑據,可以通過google認證服務注冊 4 ClientId = "", 5 ClientSecret = "", 6 });
其二我們需要設計實現登錄邏輯,通常情況下我們在登錄論壇的時候,旁邊可能會有基於QQ登錄或者別的三方認證提供商。
1 public ActionResult GoogleLogin(string returnUrl) 2 { // 創建AuthenticationProperties對象,我們可以理解為認證復制信息字典 3 var properties = new AuthenticationProperties 4 { 5 RedirectUri = Url.Action("GoogleLoginCallback", 6 new { returnUrl = returnUrl }) 7 }; 8 // 初始化google認證相關輔助信息 9 HttpContext.GetOwinContext().Authentication.Challenge(properties, "Google"); 10 // 返回401 11 return new HttpUnauthorizedResult(); 12 }
以上代碼比較簡單,我也做了相應的注釋,其邏輯是初始化google認證的一些輔助信息,然后返回401狀態碼,繼而重定向到google登錄頁。下面我們看看登錄成功之后的代碼邏輯。
1 public async Task<ActionResult> GoogleLoginCallback(string returnUrl) 2 { 3 // 從google認證服務獲取claims 4 ExternalLoginInfo loginInfo = await AuthManager.GetExternalLoginInfoAsync(); 5 // 檢查該用戶是否首次登錄系統 6 AppUser user = await UserManager.FindAsync(loginInfo.Login); 7 if (user == null) 8 { 9 user = new AppUser 10 { 11 Email = loginInfo.Email, 12 UserName = loginInfo.DefaultUserName, 13 City = Cities.Shanghai, 14 Country = Countries.China 15 }; 16 // 持久化用戶數據 17 IdentityResult result = await UserManager.CreateAsync(user); 18 // 緩存 19 result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login); 20 } 21 ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user, 22 DefaultAuthenticationTypes.ApplicationCookie); 23 ident.AddClaims(loginInfo.ExternalIdentity.Claims); 24 // 創建用戶ClaimsIdentity對象 25 AuthManager.SignIn(new AuthenticationProperties 26 { 27 IsPersistent = false 28 }, ident); 29 return Redirect(returnUrl ?? "/"); 30 }
以上就是三方認證的實現方式,下面我們通過Katana源碼看看三方認證的實現原理。通過上面Katana的cookies認證,我們了解到認證中間件的認證邏輯是實現在相應的AuthenticationHandler里面,我們同樣以google為例,去看看內部的實現。下面我們一起來上面注冊的認證中間件GoogleOAuth2AuthenticationMiddleware的定義。
1 public class GoogleOAuth2AuthenticationMiddleware : AuthenticationMiddleware<GoogleOAuth2AuthenticationOptions> 2 { 3 // 其他成員 4 public GoogleOAuth2AuthenticationMiddleware( 5 OwinMiddleware next, 6 IAppBuilder app, 7 GoogleOAuth2AuthenticationOptions options) 8 : base(next, options); 9 // 構建認證handler 10 protected override AuthenticationHandler<GoogleOAuth2AuthenticationOptions> CreateHandler() 11 { 12 return new GoogleOAuth2AuthenticationHandler(_httpClient, _logger); 13 } 14 // 構建httpclienthandler 15 private static HttpMessageHandler ResolveHttpMessageHandler(GoogleOAuth2AuthenticationOptions options); 16 }
根據以上代碼片段我們了解到,GoogleOAuth2AuthenticationMiddleware中間件似乎比我們常規的cookies認證多了一個方法ResolveHttpMessageHandler,其實這個方法沒有別的套路,就是輔助創建httpclient對象,完成http請求而已,在handler的認證邏輯里面需要獲取googletoken,就是通過它來獲取的。
第二個方法CreateHandler返回的GoogleOAuth2AuthenticationHandler對象就是我們接下來要重點討論的對象。
1 internal class GoogleOAuth2AuthenticationHandler : AuthenticationHandler<GoogleOAuth2AuthenticationOptions> 2 { 3 private const string TokenEndpoint = "https://accounts.google.com/o/oauth2/token"; 4 private const string UserInfoEndpoint = "https://www.googleapis.com/plus/v1/people/me"; 5 private const string AuthorizeEndpoint = "https://accounts.google.com/o/oauth2/auth"; 6 7 private readonly ILogger _logger; 8 private readonly HttpClient _httpClient; 9 10 public GoogleOAuth2AuthenticationHandler(HttpClient httpClient, ILogger logger) 11 { 12 _httpClient = httpClient; 13 _logger = logger; 14 } 15 // 通過httpclient訪問google認證服務器獲取token,根據token數據包裝Claim 16 protected override async Task<AuthenticationTicket> AuthenticateCoreAsync(); 17 // 如果未認證,401授權失敗發生重定向 18 protected override Task ApplyResponseChallengeAsync(); 19 20 public override async Task<bool> InvokeAsync() 21 { 22 return await InvokeReplyPathAsync(); 23 } 24 // 調用signin,保存用戶信息 25 private async Task<bool> InvokeReplyPathAsync(); 26 }
代碼比較長,我把具體實現刪掉了,實現邏輯我注釋到了方法上面,有興趣的朋友可以自己多看看源碼。以上就是NET平台上面一些主流的認證方式和實現原理。接下來我們繼續介紹ASPNETCORE的認證。
ASPNETCORE認證
熟悉微軟web平台認證授權體系的朋友應該知道,不管是早期的Forms還是Katana的cookies甚至是我接下來要介紹的ASPNETCORE基於cookies認證,其實整體的設計邏輯大致都差不多,只是具體實現上的區別,尤其是OWin的設計理念,當然現在我們幾乎已經模糊了OWin的慨念,但是在ASPNETCORE平台上到處都有它的縮影。下面我們一起來看看ASPNETCOREMVC的認證機制。
在這里,整個認證邏輯我就直接用一張圖展示:
畫圖工具是網上在線編輯的,畫的不好,別見怪。下面我簡單解釋一下認證授權流程圖,以cookies認證為例。
1.認證中間件調用CookieAuthenticationHandler實現認證,如果認證成功設置HttpContext.Use對象。
2.在執行controller中的action之前,執行授權filter,如果有設置授權filter特性。
3.如果controller或者action上沒有授權filter,直接執行action,呈現view。
4.如果有定義授權filter特性,授權過濾器再次檢查用戶是否認證,並且合並Claim,因為可以指定多個認證scheme,認證階段使用的是默認的sheme。
5.認證失敗,授權filter設置context.Result為Challenge,在后續cookie認證中間件會發生重定向到login頁面。
6.認證成功,授權失敗,授權filter設置context.Result為Forbid,在后續cookie認證中間件會發生重定向到權限不足頁面。
7.認證、授權都通過,最后顯示view。
以上就是ASPNETCOREMVC認證授權的主要執行邏輯。接下來我們一起看看,基於COREMVC的cookies認證的應用以及內部實現。
熟悉ASPNETCORE平台開發的朋友應該知道,基礎功能模塊的配置初始化,一般分為兩部曲,注冊服務、配置中間件。當然這少不了NETCORE內置DI容器的功勞,我們將要介紹的認證系統也不例外。下面我們具體看看認證系統的配置,通過Startup類型配置,關於startup的提供機制可以看看我上一篇博客,有詳細介紹。
第一部曲服務配置
1 public static void AddAuthentication(this IServiceCollection services) 2 { 3 4 // 其他代碼 5 var authenticationBuilder = services.AddAuthentication(options => 6 { 7 options.DefaultChallengeScheme = AuthenticationDefaults.AuthenticationScheme; 8 options.DefaultScheme = AuthenticationDefaults.AuthenticationScheme; 9 options.DefaultSignInScheme = AuthenticationDefaults.ExternalAuthenticationScheme; 10 }) 11 .AddCookie(AuthenticationDefaults.AuthenticationScheme, options => 12 { 13 options.Cookie.Name = $"{CookieDefaults.Prefix}{NopCookieDefaults.AuthenticationCookie}"; 14 options.Cookie.HttpOnly = true; 15 options.LoginPath = AuthenticationDefaults.LoginPath; 16 options.AccessDeniedPath = AuthenticationDefaults.AccessDeniedPath; 17 }) 18 .AddCookie(AuthenticationDefaults.ExternalAuthenticationScheme, options => 19 { 20 options.Cookie.Name = $"{CookieDefaults.Prefix}{CookieDefaults.ExternalAuthenticationCookie}"; 21 options.Cookie.HttpOnly = true; 22 options.LoginPath = AuthenticationDefaults.LoginPath; 23 options.AccessDeniedPath = AuthenticationDefaults.AccessDeniedPath; 24 }); 25 }
以上代碼片段就完成了cookies認證的所需服務注冊。其實際就是注冊cookies認證所需的基礎對象和輔助配置信息到DI容器,以便中間件可以通過DI容器方便獲取。AddAuthentication擴展方法,主要是注冊認證系統所需基礎對象。AddCookie擴展方法主要是注冊具體cookie認證Handler對象以及通過options模式配置輔助信息。
第二部曲中間件注冊
1 public static void UseAuthentication(this IApplicationBuilder application) 2 { 3 // 其他代碼 4 application.UseMiddleware<AuthenticationMiddleware>(); 5 }
認證中間件的注冊就這么一句代碼,實際就是ASPNETCORE請求管道添加認證中間件,最后通過Build初始化到這個請求管道,后續所有的請求都會通過這個認證中間件的invoke方法處理,然后傳遞下一個中間件,關於中間件的原理也可以看我上一篇帖子。認證系統的配置我們已經准備完成,下面我們看看系統登錄。
登錄
1 [HttpPost] 2 public virtual IActionResult Login(LoginModel model, string returnUrl, bool captchaValid) 3 { 4 5 // 其他代碼 6 if (ModelState.IsValid) 7 { 8 var loginResult = _userService.ValidateUser(model.Username, model.Password); 9 switch (loginResult) 10 { 11 case LoginResults.Successful: 12 { 13 var user = _userService.GetUserByUserName(model.Username); 14 15 _authenticationService.SignIn(user, model.RememberMe); 16 17 return Redirect(returnUrl); 18 } 19 } 20 } 21 22 return View(model); 23 }
以上登錄代碼片段比較簡單,主要完成兩個動作,1.收集用戶輸入的用戶名&密碼等信息,然后通過我們系統的存儲介質,校驗用戶名&密碼的合法性。2.登錄到我們的認證系統,實現我們核心登錄邏輯是SignIn方法里面。下面我們繼續看看SignIn方法的具體實現。
1 public virtual async void SignIn(User user, bool isPersistent) 2 { 3 // 其他代碼 4 // 創建身份信息集合 5 var claims = new List<Claim>(); 6 7 if (!string.IsNullOrEmpty(user.Username)) 8 claims.Add(new Claim(ClaimTypes.Name, user.Username, ClaimValueTypes.String, AuthenticationDefaults.ClaimsIssuer)); 9 10 if (!string.IsNullOrEmpty(user.Email)) 11 claims.Add(new Claim(ClaimTypes.Email, user.Email, ClaimValueTypes.Email, AuthenticationDefaults.ClaimsIssuer)); 12 13 var userIdentity = new ClaimsIdentity(claims, AuthenticationDefaults.AuthenticationScheme); 14 var userPrincipal = new ClaimsPrincipal(userIdentity); 15 // 輔助信息 16 var authenticationProperties = new AuthenticationProperties 17 { 18 IsPersistent = isPersistent, 19 IssuedUtc = DateTime.UtcNow 20 }; 21 // 創建cookie ticket,以備寫入response輸出到客戶端 22 await _httpContextAccessor.HttpContext.SignInAsync(AuthenticationDefaults.AuthenticationScheme, userPrincipal, authenticationProperties); 23 }
以上代碼片段就完成了我們認證系統的登錄。大致邏輯是構建身份聲明信息,調用HttpContext的SignInAsync方法創建ticket,在endrequest階段創建cookie寫入response。以上就是我們基於ASPNETCORE平台開發web應用對於認證的真實應用。接下來我們重點看看平台的內部實現。
Cookies認證內部實現
我們還是從服務注冊開始吧,畢竟它是完成認證系統的基石。我們把視線轉移到上面的AddAuthentication方法,注冊服務,我們看看它到底為我們的認證系統注冊了哪些基礎服務,看NETCORE源代碼。
1 public static AuthenticationBuilder AddAuthentication(this IServiceCollection services) 2 { 3 // 其他代碼 4 services.AddAuthenticationCore(); 5 services.AddDataProtection(); 6 services.AddWebEncoders(); 7 services.TryAddSingleton<ISystemClock, SystemClock>(); 8 return new AuthenticationBuilder(services); 9 }
從以上代碼片段了解到,我們的認證服務注冊是在平台AddAuthenticationCore方法里面完成的。我們一起看看AddAuthenticationCore方法的實現。
1 public static IServiceCollection AddAuthenticationCore(this IServiceCollection services) 2 { 3 services.TryAddScoped<IAuthenticationService, AuthenticationService>(); 4 services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>(); // Can be replaced with scoped ones that use DbContext 5 services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>(); 6 services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>(); 7 return services; 8 }
AddAuthenticationCore方法里面主要注冊了我們NETCORE認證系統的三個基礎對象,你可以把它們理解為黑幫的一個老大兩個堂主,由它們吩咐下面的小弟完成任務,言歸正傳這三個對象也是完成我們NETCORE平台認證的三劍客,通過Provider模式實現,下面我們一個個來介紹,我們先看看IAuthenticationService接口的定義。
1 public interface IAuthenticationService 2 { 3 Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme); 4 5 Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties); 6 7 Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties); 8 9 Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties); 10 11 Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties); 12 }
IAuthenticationService接口定義了5個方法成員,它本身不實現任何認證邏輯,只是為IAuthenticationSchemeProvider 和 IAuthenticationHandlerProvider這兩個Provider實現了封裝,提供認證服務的統一接口。下面我大概解釋一下這個5個方法在認證服務中的作用。
1.SignInAsync 登錄操作,如果登錄成功,生成加密ticket,用來標識用戶的身份。
2.SignOutAsync 退出登錄,清除Coookie等。
3.AuthenticateAsync 解密cookie,獲取ticket並驗證,最后返回一個 AuthenticateResult 對象,表示用戶的身份。
4.ChallengeAsync 未認證,返回 401 狀態碼。
5.ForbidAsync 權限不足,返回 403 狀態碼。
下面我們一起看看它的唯一默認實現類AuthenticationService。
1 public class AuthenticationService : IAuthenticationService 2 { 3 4 // 其他成員 5 public AuthenticationService(IAuthenticationSchemeProvider schemes, IAuthenticationHandlerProvider handlers, IClaimsTransformation transform); 6 7 public IAuthenticationSchemeProvider Schemes { get; } 8 9 public IAuthenticationHandlerProvider Handlers { get; } 10 11 public IClaimsTransformation Transform { get; } 12 13 public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme); 14 15 16 public virtual async Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties) 17 { 18 if (scheme == null) 19 { 20 var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync(); 21 scheme = defaultChallengeScheme?.Name; 22 if (scheme == null) 23 { 24 throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found."); 25 } 26 } 27 28 var handler = await Handlers.GetHandlerAsync(context, scheme); 29 if (handler == null) 30 { 31 throw await CreateMissingHandlerException(scheme); 32 } 33 34 await handler.ChallengeAsync(properties); 35 } 36 37 public virtual async Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties); 38 39 public virtual async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties); 40 41 public virtual async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties); 42 }
代碼比較多,我刪掉了大部分,其實現邏輯都差不多。我們以ChallengeAsync方法為例,先獲取相應的scheme,然后獲取對應的Handler,最后執行Handler的同名方法。也就說明,真正的認證邏輯是在Handler里面完成的。從AuthenticationService的定義了解到,AuthenticationService的創建是基於Handlers和schemes創建的,下面我們看看認證的第二個基礎對象IAuthenticationSchemeProvider。
1 public interface IAuthenticationSchemeProvider 2 { 3 Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync(); 4 5 Task<AuthenticationScheme> GetSchemeAsync(string name); 6 7 Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync(); 8 9 Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync(); 10 11 Task<AuthenticationScheme> GetDefaultForbidSchemeAsync(); 12 13 Task<AuthenticationScheme> GetDefaultSignInSchemeAsync(); 14 15 Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync(); 16 17 void AddScheme(AuthenticationScheme scheme); 18 19 void RemoveScheme(string name); 20 21 Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync(); 22 }
scheme其實際就是提供認證方案標識,我們知道,NETCORE的認證系統所支持的認證方案非常豐富,比如openid、bearer、cookie等等。下面我們一起看看它的默認實現AuthenticationSchemeProvider對象。
1 public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider 2 { 3 public AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options) 4 : this(options, new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal)) 5 { 6 } 7 8 protected AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes) 9 { 10 _options = options.Value; 11 12 _schemes = schemes ?? throw new ArgumentNullException(nameof(schemes)); 13 _requestHandlers = new List<AuthenticationScheme>(); 14 15 foreach (var builder in _options.Schemes) 16 { 17 var scheme = builder.Build(); 18 AddScheme(scheme); 19 } 20 } 21 22 private readonly AuthenticationOptions _options; 23 private readonly object _lock = new object(); 24 private readonly IDictionary<string, AuthenticationScheme> _schemes; 25 private readonly List<AuthenticationScheme> _requestHandlers; 26 private IEnumerable<AuthenticationScheme> _schemesCopy = Array.Empty<AuthenticationScheme>(); 27 private IEnumerable<AuthenticationScheme> _requestHandlersCopy = Array.Empty<AuthenticationScheme>(); 28 29 private Task<AuthenticationScheme> GetDefaultSchemeAsync() 30 => _options.DefaultScheme != null 31 ? GetSchemeAsync(_options.DefaultScheme) 32 : Task.FromResult<AuthenticationScheme>(null); 33 34 public virtual Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync() 35 => _options.DefaultAuthenticateScheme != null 36 ? GetSchemeAsync(_options.DefaultAuthenticateScheme) 37 : GetDefaultSchemeAsync(); 38 39 public virtual Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync() 40 => _options.DefaultChallengeScheme != null 41 ? GetSchemeAsync(_options.DefaultChallengeScheme) 42 : GetDefaultSchemeAsync(); 43 44 public virtual Task<AuthenticationScheme> GetDefaultForbidSchemeAsync() 45 => _options.DefaultForbidScheme != null 46 ? GetSchemeAsync(_options.DefaultForbidScheme) 47 : GetDefaultChallengeSchemeAsync(); 48 49 public virtual Task<AuthenticationScheme> GetDefaultSignInSchemeAsync() 50 => _options.DefaultSignInScheme != null 51 ? GetSchemeAsync(_options.DefaultSignInScheme) 52 : GetDefaultSchemeAsync(); 53 54 public virtual Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync() 55 => _options.DefaultSignOutScheme != null 56 ? GetSchemeAsync(_options.DefaultSignOutScheme) 57 : GetDefaultSignInSchemeAsync(); 58 59 public virtual Task<AuthenticationScheme> GetSchemeAsync(string name) 60 => Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null); 61 62 public virtual Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync() 63 => Task.FromResult(_requestHandlersCopy); 64 65 public virtual void AddScheme(AuthenticationScheme scheme) 66 { 67 if (_schemes.ContainsKey(scheme.Name)) 68 { 69 throw new InvalidOperationException("Scheme already exists: " + scheme.Name); 70 } 71 lock (_lock) 72 { 73 if (_schemes.ContainsKey(scheme.Name)) 74 { 75 throw new InvalidOperationException("Scheme already exists: " + scheme.Name); 76 } 77 if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType)) 78 { 79 _requestHandlers.Add(scheme); 80 _requestHandlersCopy = _requestHandlers.ToArray(); 81 } 82 _schemes[scheme.Name] = scheme; 83 _schemesCopy = _schemes.Values.ToArray(); 84 } 85 } 86 87 public virtual void RemoveScheme(string name); 88 89 public virtual Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync() 90 => Task.FromResult(_schemesCopy); 91 }
從AuthenticationSchemeProvider的默認實現來看,它主要是提供scheme管理。從AuthenticationSchemeProvider構造器的定義來看,它的初始化是由我們注冊服務時所提供的options配置對象提供,其最終初始化體現在AddScheme方法上,也就是對所有注冊的scheme添加集合,所有scheme最終體現為一個AuthenticationScheme對象,下面我們看看它的定義。
1 public class AuthenticationScheme 2 { 3 public AuthenticationScheme(string name, string displayName, Type handlerType) 4 { 5 // 其他代碼 6 if (!typeof(IAuthenticationHandler).IsAssignableFrom(handlerType)) 7 { 8 throw new ArgumentException("handlerType must implement 9 IAuthenticationHandler."); 10 } 11 12 Name = name; 13 HandlerType = handlerType; 14 DisplayName = displayName; 15 } 16 17 public string Name { get; } 18 19 public string DisplayName { get; } 20 21 public Type HandlerType { get; } 22 }
每一個scheme里面都包含了對應的Handler,同時派生自IAuthenticationHandler。這個handler就是后續真正處理我們的認證實現。下面我們一起看看認證基石的第三個對象IAuthenticationHandlerProvider的定義。
1 public interface IAuthenticationHandlerProvider 2 { 3 Task<IAuthenticationHandler> GetHandlerAsync(HttpContext context, string authenticationScheme); 4 }
這個接口的定義很簡單,就一個成員,GetHandlerAsync方法,顧名思義就是獲取authenticationScheme對應的Handler,我們看看IAuthenticationHandlerProvider的默認實現。
1 public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider 2 { 3 public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes) 4 { 5 Schemes = schemes; 6 } 7 8 public IAuthenticationSchemeProvider Schemes { get; } 9 10 private Dictionary<string, IAuthenticationHandler> _handlerMap = new Dictionary<string, IAuthenticationHandler>(StringComparer.Ordinal); 11 12 public async Task<IAuthenticationHandler> GetHandlerAsync(HttpContext context, string authenticationScheme) 13 { 14 if (_handlerMap.ContainsKey(authenticationScheme)) 15 { 16 return _handlerMap[authenticationScheme]; 17 } 18 19 var scheme = await Schemes.GetSchemeAsync(authenticationScheme); 20 if (scheme == null) 21 { 22 return null; 23 } 24 var handler = (context.RequestServices.GetService(scheme.HandlerType) ?? 25 ActivatorUtilities.CreateInstance(context.RequestServices, scheme.HandlerType)) 26 as IAuthenticationHandler; 27 if (handler != null) 28 { 29 await handler.InitializeAsync(scheme, context); 30 _handlerMap[authenticationScheme] = handler; 31 } 32 return handler; 33 } 34 }
GetHandlerAsync方法的實現邏輯也比較簡單,首先通過_handlerMap字典根據scheme名稱獲取,一般首次獲取,都是null。然后通過schemeprovide獲取對應的scheme,通過上面分析我們知道,scheme體現為一個AuthenticationScheme對象,里面包含了handlertype。最后創建這個handler,創建handler有兩種情況,第一種從DI容器獲取,第二種情況反射創建,最終返回的是有如下定義的IAuthenticationHandler接口。
1 public interface IAuthenticationHandler 2 { 3 Task InitializeAsync(AuthenticationScheme scheme, HttpContext context); 4 5 Task<AuthenticateResult> AuthenticateAsync(); 6 7 Task ChallengeAsync(AuthenticationProperties properties); 8 9 Task ForbidAsync(AuthenticationProperties properties); 10 }
該接口就是實打實干實事的,我們的認證邏輯就是通過該handler實現的。AuthenticateAsync方法就是我們的認證入口,其返回類型是一個AuthenticateResult類型,也就是我們的認證結果,接下來我們看看它的定義。
1 public class AuthenticateResult 2 { 3 protected AuthenticateResult() { } 4 5 public bool Succeeded => Ticket != null; 6 7 public AuthenticationTicket Ticket { get; protected set; } 8 9 public ClaimsPrincipal Principal => Ticket?.Principal; 10 11 public AuthenticationProperties Properties { get; protected set; } 12 13 public Exception Failure { get; protected set; } 14 15 public bool None { get; protected set; } 16 17 public static AuthenticateResult Success(AuthenticationTicket ticket) 18 { 19 if (ticket == null) 20 { 21 throw new ArgumentNullException(nameof(ticket)); 22 } 23 return new AuthenticateResult() { Ticket = ticket, Properties = ticket.Properties }; 24 } 25 26 public static AuthenticateResult NoResult() 27 { 28 return new AuthenticateResult() { None = true }; 29 } 30 31 public static AuthenticateResult Fail(Exception failure) 32 { 33 return new AuthenticateResult() { Failure = failure }; 34 } 35 36 public static AuthenticateResult Fail(Exception failure, AuthenticationProperties properties) 37 { 38 return new AuthenticateResult() { Failure = failure, Properties = properties }; 39 } 40 41 public static AuthenticateResult Fail(string failureMessage) 42 => Fail(new Exception(failureMessage)); 43 44 public static AuthenticateResult Fail(string failureMessage, AuthenticationProperties properties) 45 => Fail(new Exception(failureMessage), properties); 46 }
如上代碼,AuthenticateResult對象的定義邏輯很簡單,就是包裝認證結果信息,比如AuthenticationTicket,它主要定義了我們的基本認證信息,我們可以把它理解為一張認證后的票據信息。AuthenticationProperties類型它主要定義了我們認證相關的輔助信息,其中包括過期、重定向、持久等等信息。關於這兩個類型的定義我就不貼代碼了,其實現比較簡單。兜兜轉轉終於到了我們的cooke認證實現類CookieAuthenticationHandler對象,下面我們一起看看它的定義。
1 public class CookieAuthenticationHandler : SignInAuthenticationHandler<CookieAuthenticationOptions> 2 { 3 // 其他代碼 4 public CookieAuthenticationHandler(IOptionsMonitor<CookieAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) 5 : base(options, logger, encoder, clock) 6 { } 7 8 protected override async Task<AuthenticateResult> HandleAuthenticateAsync(); 9 10 protected virtual async Task FinishResponseAsync(); 11 12 protected async override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties); 13 14 protected async override Task HandleSignOutAsync(AuthenticationProperties properties); 15 16 protected override async Task HandleForbiddenAsync(AuthenticationProperties properties); 17 18 protected override async Task HandleChallengeAsync(AuthenticationProperties properties); 19 }
我們暫且先不討論CookieAuthenticationHandler認證實現邏輯,因為整個認證結構,有涉及多個Handler對象,我們還是一步一步按照這個層次結構來介紹吧,至少大家不會覺得突兀。從CookieAuthenticationHandler的定義來看,它並未直接實現IAuthenticationHandler,還是實現了有着如下定義的SignInAuthenticationHandler接口對象。
1 public abstract class SignInAuthenticationHandler<TOptions> : SignOutAuthenticationHandler<TOptions>, IAuthenticationSignInHandler 2 where TOptions : AuthenticationSchemeOptions, new() 3 { 4 public SignInAuthenticationHandler(IOptionsMonitor<TOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) 5 { } 6 7 public virtual Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) 8 { 9 var target = ResolveTarget(Options.ForwardSignIn); 10 return (target != null) 11 ? Context.SignInAsync(target, user, properties) 12 : HandleSignInAsync(user, properties ?? new AuthenticationProperties()); 13 } 14 15 protected abstract Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties); 16 }
從該對象的定義來看,它就是負責處理登錄相關處理的。其中還有SignOutAuthenticationHandler,處理邏輯類似,負責登出操作,可能有些朋友會覺得有點奇怪,為什么登入登出會單獨定義成相關接口,個人理解,其一站在業務的角度,登入、登出和認證還是有一定的獨立性,並非所有業務場景必須要先登錄才能實現認證,而且認證更多關注的是過程,其二以適應更多認證方式,把登入登出抽象出來,使其擴展更方便。它們派生自抽象類AuthenticationHandler<TOptions>,下面我們看看它的定義。
1 public abstract class AuthenticationHandler<TOptions> : IAuthenticationHandler where TOptions : AuthenticationSchemeOptions, new() 2 { 3 // 其他代碼 4 protected AuthenticationHandler(IOptionsMonitor<TOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) 5 { 6 Logger = logger.CreateLogger(this.GetType().FullName); 7 UrlEncoder = encoder; 8 Clock = clock; 9 OptionsMonitor = options; 10 } 11 12 public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) 13 { 14 if (scheme == null) 15 { 16 throw new ArgumentNullException(nameof(scheme)); 17 } 18 if (context == null) 19 { 20 throw new ArgumentNullException(nameof(context)); 21 } 22 23 Scheme = scheme; 24 Context = context; 25 26 Options = OptionsMonitor.Get(Scheme.Name); 27 28 await InitializeEventsAsync(); 29 await InitializeHandlerAsync(); 30 } 31 32 protected virtual async Task InitializeEventsAsync() 33 { 34 Events = Options.Events; 35 if (Options.EventsType != null) 36 { 37 Events = Context.RequestServices.GetRequiredService(Options.EventsType); 38 } 39 Events = Events ?? await CreateEventsAsync(); 40 } 41 42 protected virtual Task<object> CreateEventsAsync() => Task.FromResult(new object()); 43 44 protected virtual Task InitializeHandlerAsync() => Task.CompletedTask; 45 46 protected string BuildRedirectUri(string targetPath) 47 => Request.Scheme + "://" + Request.Host + OriginalPathBase + targetPath; 48 49 protected virtual string ResolveTarget(string scheme) 50 { 51 var target = scheme ?? Options.ForwardDefaultSelector?.Invoke(Context) ?? Options.ForwardDefault; 52 53 // Prevent self targetting 54 return string.Equals(target, Scheme.Name, StringComparison.Ordinal) 55 ? null 56 : target; 57 } 58 59 public async Task<AuthenticateResult> AuthenticateAsync() 60 { 61 var target = ResolveTarget(Options.ForwardAuthenticate); 62 if (target != null) 63 { 64 return await Context.AuthenticateAsync(target); 65 } 66 67 var result = await HandleAuthenticateOnceAsync(); 68 if (result?.Failure == null) 69 { 70 var ticket = result?.Ticket; 71 if (ticket?.Principal != null) 72 { 73 Logger.AuthenticationSchemeAuthenticated(Scheme.Name); 74 } 75 else 76 { 77 Logger.AuthenticationSchemeNotAuthenticated(Scheme.Name); 78 } 79 } 80 else 81 { 82 Logger.AuthenticationSchemeNotAuthenticatedWithFailure(Scheme.Name, result.Failure.Message); 83 } 84 return result; 85 } 86 87 protected Task<AuthenticateResult> HandleAuthenticateOnceAsync() 88 { 89 if (_authenticateTask == null) 90 { 91 _authenticateTask = HandleAuthenticateAsync(); 92 } 93 94 return _authenticateTask; 95 } 96 97 protected async Task<AuthenticateResult> HandleAuthenticateOnceSafeAsync() 98 { 99 try 100 { 101 return await HandleAuthenticateOnceAsync(); 102 } 103 catch (Exception ex) 104 { 105 return AuthenticateResult.Fail(ex); 106 } 107 } 108 109 protected abstract Task<AuthenticateResult> HandleAuthenticateAsync(); 110 111 protected virtual Task HandleForbiddenAsync(AuthenticationProperties properties) 112 { 113 Response.StatusCode = 403; 114 return Task.CompletedTask; 115 } 116 117 protected virtual Task HandleChallengeAsync(AuthenticationProperties properties) 118 { 119 Response.StatusCode = 401; 120 return Task.CompletedTask; 121 } 122 123 public async Task ChallengeAsync(AuthenticationProperties properties) 124 { 125 var target = ResolveTarget(Options.ForwardChallenge); 126 if (target != null) 127 { 128 await Context.ChallengeAsync(target, properties); 129 return; 130 } 131 132 properties = properties ?? new AuthenticationProperties(); 133 await HandleChallengeAsync(properties); 134 Logger.AuthenticationSchemeChallenged(Scheme.Name); 135 } 136 137 public async Task ForbidAsync(AuthenticationProperties properties) 138 { 139 var target = ResolveTarget(Options.ForwardForbid); 140 if (target != null) 141 { 142 await Context.ForbidAsync(target, properties); 143 return; 144 } 145 146 properties = properties ?? new AuthenticationProperties(); 147 await HandleForbiddenAsync(properties); 148 Logger.AuthenticationSchemeForbidden(Scheme.Name); 149 } 150 }
該抽象類直接實現了我們上面提到的認證接口IAuthenticationHandler,是NETCORE所有認證類的基類,並且提供相關默認實現。抽象方法HandleAuthenticateAsync就是我們認證處理的入口,也是認證的核心實現,由具體的認證實現類實現。該基類的其他方法,邏輯都比較簡單,或者只提供默認實現就不再贅述,接下來我們圍繞上面提到的cookie認證的核心實現類CookieAuthenticationHandler介紹其具體認證實現,在介紹其具體實現之前,我們來看看它是如何被創建的或者說被注入到我們的DI容器的,其實是通過Startup的ConfigureServices方法注冊進來的,看代碼。
1 public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<CookieAuthenticationOptions> configureOptions) 2 { 3 builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>()); 4 builder.Services.AddOptions<CookieAuthenticationOptions>(authenticationScheme).Validate(o => o.Cookie.Expiration == null, "Cookie.Expiration is ignored, use ExpireTimeSpan instead."); 5 return builder.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>(authenticationScheme, displayName, configureOptions); 6 }
通過CookieExtensions的擴展方法AddCookie方法注入進來的,有疑惑的朋友可以看看我在開始介紹NETCORE認證的開始部分就貼出了這段代碼。接下來我們繼續看認證核心部分。
1 public class CookieAuthenticationHandler : SignInAuthenticationHandler<CookieAuthenticationOptions> 2 { 3 // 其他成員 4 public CookieAuthenticationHandler(IOptionsMonitor<CookieAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) 5 : base(options, logger, encoder, clock) 6 { } 7 8 protected new CookieAuthenticationEvents Events 9 { 10 get { return (CookieAuthenticationEvents)base.Events; } 11 set { base.Events = value; } 12 } 13 // 初始化handler,設置響應cookie回調 14 protected override Task InitializeHandlerAsync() 15 { 16 // Cookies needs to finish the response 17 Context.Response.OnStarting(FinishResponseAsync); 18 return Task.CompletedTask; 19 } 20 // cookie認證各個處理階段,默認注冊的事件 21 protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new CookieAuthenticationEvents()); 22 // 獲取ticket票據 23 private Task<AuthenticateResult> EnsureCookieTicket(); 24 // 刷新票據 25 private void CheckForRefresh(AuthenticationTicket ticket); 26 27 private void RequestRefresh(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal = null); 28 // clone票據 29 private AuthenticationTicket CloneTicket(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal); 30 31 private async Task<AuthenticateResult> ReadCookieTicket(); 32 // cookie認證 33 protected override async Task<AuthenticateResult> HandleAuthenticateAsync() 34 { 35 // 獲取票據 36 var result = await EnsureCookieTicket(); 37 if (!result.Succeeded) 38 { 39 return result; 40 } 41 // 用戶信息驗證,默認沒有任何邏輯實現 42 var context = new CookieValidatePrincipalContext(Context, Scheme, Options, result.Ticket); 43 await Events.ValidatePrincipal(context); 44 45 if (context.Principal == null) 46 { 47 return AuthenticateResult.Fail("No principal."); 48 } 49 // 更新ticket票據,一般在之前會更新用戶信息 50 if (context.ShouldRenew) 51 { 52 RequestRefresh(result.Ticket, context.Principal); 53 } 54 // 認證成功,包裝result返回 55 return AuthenticateResult.Success(new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name)); 56 } 57 // 寫入response 58 protected virtual async Task FinishResponseAsync(); 59 // 登錄, 60 protected async override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) 61 { 62 if (user == null) 63 { 64 throw new ArgumentNullException(nameof(user)); 65 } 66 // 獲取配置信息 67 properties = properties ?? new AuthenticationProperties(); 68 69 _signInCalled = true; 70 71 // 初始化,比如sessionkey 72 await EnsureCookieTicket(); 73 var cookieOptions = BuildCookieOptions(); 74 // 創建cookiecontext,以備寫入response 75 var signInContext = new CookieSigningInContext( 76 Context, 77 Scheme, 78 Options, 79 user, 80 properties, 81 cookieOptions); 82 // 設置認證輔助信息,比如過期時間等等。 83 DateTimeOffset issuedUtc; 84 if (signInContext.Properties.IssuedUtc.HasValue) 85 { 86 issuedUtc = signInContext.Properties.IssuedUtc.Value; 87 } 88 else 89 { 90 issuedUtc = Clock.UtcNow; 91 signInContext.Properties.IssuedUtc = issuedUtc; 92 } 93 94 if (!signInContext.Properties.ExpiresUtc.HasValue) 95 { 96 signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan); 97 } 98 // 執行signin階段處理事件,如果有重寫,執行重寫邏輯 99 await Events.SigningIn(signInContext); 100 // 是否持久化 101 if (signInContext.Properties.IsPersistent) 102 { 103 var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan); 104 signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime(); 105 } 106 // 創建認證票據ticket 107 var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.Scheme.Name); 108 // 基於session邏輯,實現復雜的cookie信息緩存到服務端 109 if (Options.SessionStore != null) 110 { 111 if (_sessionKey != null) 112 { 113 await Options.SessionStore.RemoveAsync(_sessionKey); 114 } 115 _sessionKey = await Options.SessionStore.StoreAsync(ticket); 116 var principal = new ClaimsPrincipal( 117 new ClaimsIdentity( 118 new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, 119 Options.ClaimsIssuer)); 120 ticket = new AuthenticationTicket(principal, null, Scheme.Name); 121 } 122 // 加密票據 123 var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding()); 124 // 設置response響應頭 125 Options.CookieManager.AppendResponseCookie( 126 Context, 127 Options.Cookie.Name, 128 cookieValue, 129 signInContext.CookieOptions); 130 131 var signedInContext = new CookieSignedInContext( 132 Context, 133 Scheme, 134 signInContext.Principal, 135 signInContext.Properties, 136 Options); 137 // 登錄后的事件處理 138 await Events.SignedIn(signedInContext); 139 140 var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath; 141 await ApplyHeaders(shouldRedirect, signedInContext.Properties); 142 143 Logger.AuthenticationSchemeSignedIn(Scheme.Name); 144 } 145 // 登出 146 protected async override Task HandleSignOutAsync(AuthenticationProperties properties); 147 148 private async Task ApplyHeaders(bool shouldRedirectToReturnUrl, AuthenticationProperties properties); 149 // 權限不足 150 protected override async Task HandleForbiddenAsync(AuthenticationProperties properties) 151 { 152 var returnUrl = properties.RedirectUri; 153 if (string.IsNullOrEmpty(returnUrl)) 154 { 155 returnUrl = OriginalPathBase + OriginalPath + Request.QueryString; 156 } 157 var accessDeniedUri = Options.AccessDeniedPath + QueryString.Create(Options.ReturnUrlParameter, returnUrl); 158 var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(accessDeniedUri)); 159 await Events.RedirectToAccessDenied(redirectContext); 160 } 161 // 未認證用戶,訪問保護的資源 162 protected override async Task HandleChallengeAsync(AuthenticationProperties properties) 163 { 164 // 通過配置信息獲取重定向url 165 var redirectUri = properties.RedirectUri; 166 if (string.IsNullOrEmpty(redirectUri)) 167 { 168 redirectUri = OriginalPathBase + OriginalPath + Request.QueryString; 169 } 170 171 var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri); 172 var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(loginUri)); 173 // 重定向到登錄頁面 174 await Events.RedirectToLogin(redirectContext); 175 } 176 }
以上就是cookie認證的核心實現,代碼注釋比較詳細,接下來我大致描述一下cookie認證的處理邏輯,其實跟傳統的Forms或者Katana的cookie認證思路差不多。
1.首先獲取請求cookie,解密並創建ticket票據。
2.如果配置了sessionstore方案,通過sessionkey獲取用戶完整的聲明信息。
3.校驗過期,如果未過期。
4.更新cookie,條件為過期時間范圍已過半。
5.校驗用戶信息,主要是針對cookie未失效,用戶聲明信息發生變更。
6.返回AuthenticateResult認證結果對象。
以上6點就是我個人針對NETCOREcookie認證的理解。接下來我們一起看看,認證中間件是如何關聯它們,實現我們的系統認證。
認證中間件
下面我們看看認證中間件的定義。
1 public class AuthenticationMiddleware 2 { 3 #region Fields 4 5 private readonly RequestDelegate _next; 6 7 #endregion 8 9 #region Ctor 10 11 public AuthenticationMiddleware(IAuthenticationSchemeProvider schemes, RequestDelegate next) 12 { 13 Schemes = schemes ?? throw new ArgumentNullException(nameof(schemes)); 14 _next = next ?? throw new ArgumentNullException(nameof(next)); 15 } 16 17 #endregion 18 19 #region Properties 20 21 public IAuthenticationSchemeProvider Schemes { get; set; } 22 23 #endregion 24 25 #region Methods 26 27 public async Task Invoke(HttpContext context) 28 { 29 context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature 30 { 31 OriginalPath = context.Request.Path, 32 OriginalPathBase = context.Request.PathBase 33 }); 34 35 var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>(); 36 foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()) 37 { 38 try 39 { 40 if (await handlers.GetHandlerAsync(context, scheme.Name) is IAuthenticationRequestHandler handler && await handler.HandleRequestAsync()) 41 return; 42 } 43 catch 44 { 45 } 46 } 47 48 var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync(); 49 if (defaultAuthenticate != null) 50 { 51 var result = await context.AuthenticateAsync(defaultAuthenticate.Name); 52 if (result?.Principal != null) 53 { 54 context.User = result.Principal; 55 } 56 } 57 58 await _next(context); 59 } 60 61 #endregion 62 }
如上認證中間件就是這么簡單,關於中間件的原理可以參看我上一篇帖子。
1.首先從DI里面獲取IAuthenticationHandlerProvider的默認實現類AuthenticationHandlerProvider。
2.從schemes里面獲取所有實現IAuthenticationRequestHandler接口的handler,沒有什么特別的,就是多了一個請求方法,后續我會介紹,暫時我們把它理解為三方認證的實現handler。
3.如果有注冊該handler實例,將調用認證邏輯。
4.如果沒有注冊requesthandler實例,獲取默認scheme。
5.從指定的scheme里面獲取具體認證handler實現認證。
6.如果認證成功,返回result,並賦值httpcontext.user屬性,完成認證。
最后總結
本來打算把NETCORE的授權也一並講完,實在想睡覺了,今天就到這吧。下面我來做個簡單的總結吧,關於NET平台甚至NETCORE基於cookie認證的實現思路大致是一樣的,只是細節上面的區別,當然我理解的可能有些錯誤。我們學習微軟web平台的認證授權,其一是更好的掌握這個平台,其二是學習他的設計思路,當我們自己在實際開發中碰到安全相關的問題,如何去合理設計,更好的保證系統的安全性等等。