.Net Core 認證元件之Cookie認證元件解析原始碼
接著上文.Net Core 認證系統原始碼解析,Cookie認證算是常用的認證模式,但是目前主流都是前後端分離,有點雞肋但是,不考慮移動端的站點或者純管理後臺網站可以使用這種認證方式.注意:基於瀏覽器且不是前後端分離的架構(頁面端具有服務端處理能力).移動端就不要考慮了,太麻煩.支援前後端分離前給移動端提供認證Api的一般採用JwtBearer認證,可以和IdentityServer4的password模式結合.很適用,但是id4的password模式各客戶端必須絕對信任,因為要暴露使用者名稱密碼.適合做企業級下所有產品的認證.不支援除企業外的第三方呼叫.當然id4提供了其他模式.這是題外話.但是場景得介紹清楚.以免誤導大家!
1、Cookie認證流程
引入核心認證元件之後,通過擴充套件的方式引入Cookie認證,微軟採用鏈式程式設計,很優雅.Net Core的一大特點.
注入Cookie認證方案,指定Cookie認證引數,並指定Cookie認證處理器,先不介紹引數,看看處理器幹了什麼.
Cookie的核心認證方法,第一步如下:
一些必須的防重複執行操作,沒截圖,也不介紹了,安全工作,只貼核心程式碼.第一步,就是去讀取客戶端存在的cookie資訊.
微軟在Cookie認證引數中提供了介面,意味者你可以自定義讀取Cookie內容的實現,他會把上下文和Cookie的名稱傳給你,這樣就能定製獲取Cookie內容的實現.接著解密Cookie內容
微軟注入了Core的核心加密元件,大家自行百度,卻採用微軟預設的實現.所以客戶端的cookie內容一般都以加密內容顯示.
接著
拿到seesionId的cliam,關於claim不多說,自行百度.core新的身份模型.必須瞭解的內容.
cookie認證引數中你可以配置SessionStore,意味者你的session可以進行持久化管理,資料庫還是redis還是分散式環境自行選擇.應用場景是cookie過長,客戶端無法儲存,那麼就可以通過配置這個SessionStore來實現.即分散式會話.微軟也提供了擴充套件.
接著,cookie過期檢測.
接著
上面的程式碼意味著cookie可以自動重新整理.通過以下兩個引數
如果讀取到的客戶端的cookie支援過期重新整理,那麼重新寫入到客戶端.
ok,如果沒有在客戶端讀取到cookie內容,意味者cookie被清除,或者使用者是第一次登陸,直接返回認證失敗,如果成功,執行認證cookie校驗認證上下文的方法
Events可以在AuthenticationSchemeOptions引數中配置
但是Cookie認證引數提供了預設實現
意味者你可以在注入Cookie認證服務的時候,自定義驗證cookie結果的驗證實現.
通過CookieAuthenticationOptions的Events屬性進行注入.驗證完畢,
判斷上下文中的ShouldRenew引數,這個你可以根據業務需要執行重新整理cookie的實現,最後返回認證結果.
整個流程到這裡結束.
2、應用
構建登陸頁面和首頁,直接網上找了,程式碼如下:
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; namespace Core.Authentication.Test { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddControllers(); //注入核心認證元件和cookie認證元件 services.AddAuthentication(options => { options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; }).AddCookie(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseAuthentication(); app.UseAuthorize(); app.AddLoginHtml(); app.AddUserInfoHtml(); } } public static class CustomMiddleware { /// <summary> /// 登陸頁面跳過認證元件 /// </summary> /// <param name="app"></param> /// <returns></returns> public static IApplicationBuilder UseAuthorize(this IApplicationBuilder app) { return app.Use(async (context, next) => { if (context.Request.Path == "/Account/Login") { await next(); } else { var user = context.User; if (user?.Identity?.IsAuthenticated ?? false) { await next(); } else { await context.ChallengeAsync(); } } }); } /// <summary> /// 注入登陸頁面 /// </summary> /// <param name="app"></param> /// <returns></returns> public static IApplicationBuilder AddLoginHtml(this IApplicationBuilder app) { return app.Map("/Account/Login", builder => builder.Run(async context => { if (context.Request.Method == "GET") { await context.Response.WriteHtmlAsync(async res => { await res.WriteAsync($"<form method=\"post\">"); await res.WriteAsync($"<input type=\"hidden\" name=\"returnUrl\" value=\"{HttpResponseExtensions.HtmlEncode(context.Request.Query["ReturnUrl"])}\"/>"); await res.WriteAsync($"<div class=\"form-group\"><label>使用者名稱:<input type=\"text\" name=\"userName\" class=\"form-control\"></label></div>"); await res.WriteAsync($"<div class=\"form-group\"><label>密碼:<input type=\"password\" name=\"password\" class=\"form-control\"></label></div>"); await res.WriteAsync($"<button type=\"submit\" class=\"btn btn-default\">登入</button>"); await res.WriteAsync($"</form>"); }); } else { var userName = context.Request.Form["userName"]; var userPassword = context.Request.Form["password"]; if (!(userName == "admin" && userPassword == "admin")) { await context.Response.WriteHtmlAsync(async res => { await res.WriteAsync($"<h1>使用者名稱或密碼錯誤。</h1>"); await res.WriteAsync("<a class=\"btn btn-default\" href=\"/Account/Login\">返回</a>"); }); } else { //寫入Cookie var claimIdentity = new ClaimsIdentity("Cookie"); claimIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier,"1")); claimIdentity.AddClaim(new Claim(ClaimTypes.Name, userName)); claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "[email protected]")); var claimsPrincipal = new ClaimsPrincipal(claimIdentity); await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal); if (string.IsNullOrEmpty(context.Request.Form["ReturnUrl"])) context.Response.Redirect("/"); else context.Response.Redirect(context.Request.Form["ReturnUrl"]); } } })); } /// <summary> /// 注入使用者資訊頁面 /// </summary> /// <returns></returns>` public static IApplicationBuilder AddUserInfoHtml(this IApplicationBuilder app) { return app.Map("/profile", builder => builder.Run(async context => { await context.Response.WriteHtmlAsync(async res => { await res.WriteAsync($"<h1>你好,當前登入使用者: {HttpResponseExtensions.HtmlEncode(context.User.Identity.Name)}</h1>"); await res.WriteAsync("<a class=\"btn btn-default\" href=\"/Account/Logout\">退出</a>"); await res.WriteAsync($"<h2>AuthenticationType:{context.User.Identity.AuthenticationType}</h2>"); await res.WriteAsync("<h2>Claims:</h2>"); await res.WriteTableHeader(new string[] { "Claim Type", "Value" }, context.User.Claims.Select(c => new string[] { c.Type, c.Value })); }); })); } } public static class HttpResponseExtensions { public static async Task WriteHtmlAsync(this HttpResponse response, Func<HttpResponse, Task> writeContent) { var bootstrap = "<link rel=\"stylesheet\" href=\"https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css\" integrity=\"sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u\" crossorigin=\"anonymous\">"; response.ContentType = "text/html"; await response.WriteAsync($"<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"UTF-8\">{bootstrap}</head><body><div class=\"container\">"); await writeContent(response); await response.WriteAsync("</div></body></html>"); } public static async Task WriteTableHeader(this HttpResponse response, IEnumerable<string> columns, IEnumerable<IEnumerable<string>> data) { await response.WriteAsync("<table class=\"table table-condensed\">"); await response.WriteAsync("<tr>"); foreach (var column in columns) { await response.WriteAsync($"<th>{HtmlEncode(column)}</th>"); } await response.WriteAsync("</tr>"); foreach (var row in data) { await response.WriteAsync("<tr>"); foreach (var column in row) { await response.WriteAsync($"<td>{HtmlEncode(column)}</td>"); } await response.WriteAsync("</tr>"); } await response.WriteAsync("</table>"); } public static string HtmlEncode(string content) => string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content); } }
ok,開始分析程式碼,第一步:
中介軟體放行登陸介面,接著構建頁面.頁面構建完畢。看登陸方法都幹了什麼
使用者校驗通過後,生成ClaimsPrincipal身份證集合,微軟關於身份認證的模型都是基於Claim的,所以包括id4、identity登陸元件、等等裡面大量使用到了ClaimsPrincipal
接著
向瀏覽器端寫入cookie,剛剛寫的完整的流程,清了下cookie,全都沒了,醉了.吐槽一下部落格園的儲存機制,放redis也好的,清下cookie就沒了.花了這個多時間.不想在重寫一遍了.這個方法,我就大致介紹下核心點.
這個方案最終會調到,完成cookie的寫入
第一步
這個過程,可能存在重複登陸的情況.
這裡CookieAuthenticationOptions通過Cookie屬性,你可以自定義Cookie配置引數,預設實現如下:
微軟通過Builder生成器模式實現.不明白,請移步我的設計模式板塊,很簡單.
接著構建預登陸上下文
這裡CookieAuthenticationOptions通過配置Events屬性,你可以做一些持久化操作.或者修改引數,相容你的業務
接著
_sessionKey可能存在已登陸的情況,那就先清除,接著通過配置CookieAuthenticationOptions的SessionStore屬性,你可以實現會話持久化,或者分散式會話.自行選擇.
接著
向瀏覽器寫入cookie
不多說,一樣.你也可以進行持久化操作,或者修改引數
最後
寫http頭,沒啥東西.並進行日誌記錄操作.
ok,登陸的核心流程到這裡介紹,跑下demo
此時沒有cookie,輸入 admin admin登陸.
ok,登陸成功,cookie寫入完畢.清除cookie,跳轉到登陸介面.整個流程結束.純屬個人理解,能力有限,有問題,請指正,謝謝.
除遠端登陸外,其餘登陸流程(Cookie、Jwt)等都大同小異,所以接下去有時間,會分析遠端登陸的原始碼,但是不想浪費太多時間,下一張會分析微軟的
授權元件,看看他是如何和認證元件協同工作的.包括如何整合id4、identity、jwtbear完成一整套前端分離架構(且對移動端友好)的認證中心的構