1. 程式人生 > >在 ASP.NET Core 應用中使用 Cookie 進行身份認證

在 ASP.NET Core 應用中使用 Cookie 進行身份認證

## Overview 身份認證是網站最基本的功能,最近因為業務部門的一個需求,需要對一個已經存在很久的小工具網站進行改造,因為在逐步的將一些離散的系統遷移至 .NET Core,所以趁這個機會將這個老的 .NET Framework 4.0 的專案進行升級 老的專案是一個 MVC 的專案並且有外網訪問的需求,大部門的微服務平臺因為和內部的業務執行比較密切,介於資安要求與外網進行了隔離,因此本次升級就不會遷移到該平臺上進行前後端分離改造 使用頻次不高,不存在高併發,實現週期短,所以就沒有必要為了用某些元件而用,因此這裡還是選擇沿用 MVC 框架,對於網站的身份認證則採用單體應用最常見的 Cookie 認證來實現,本篇文章則是如何實現的一個基礎的教程,僅供參考 ## Step by Step 在涉及到系統許可權管理的相關內容時,必定會提到兩個長的很像的單詞,`authentication`(認證) 和 `authorization`(授權) - authentication:用一些資料來證明你就是你,登入系統、指紋、面部解鎖就是一種認證的過程 - authorization:授予一些使用者去訪問一些特殊資源或功能的過程,系統包含管理員和普通使用者兩種角色,只有管理員才可以執行某些操作,賦予管理員角色某些操作的過程就是授權 只有認證和授權一起配合,才可以完成對於整個系統的許可權管控 ### 2.1、前期準備 假定現在已經存在了一個 ASP.NET Core MVC 應用,這裡以 VS 建立的預設專案為例,對於一個 MVC or Web API 應用,要求使用者必須登入之後才能進行訪問,最簡單的方式,在需要認證的 Controller 或 Action 上新增 `Authorize` 特性,然後在 `Startup.Configure` 方法中通過 `UseAuthorization` 新增中介軟體即可 ```csharp [Authorize] public class HomeController : Controller { public IActionResult Index() { return View(); } } ``` ```csharp public class Startup { public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( "default", "{controller=Home}/{action=Index}/{id?}"); }); } } ``` 當然,當系統只包含一個兩個 Controller 時還好,當系統比較複雜的時候,再一個個的新增 `Authorize` 特性就比較麻煩了,因此這裡我們可以通過在 `Startup.ConfigureServices` 中新增全域性的 `AuthorizeFilter` 過濾器,實現對於全域性的認證管控 ```csharp public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews() .AddMvcOptions(options => { options.Filters.Add(new AuthorizeFilter()); }); } } ``` 此時,對於一些不需要進行認證就可以訪問的頁面,只需要新增 `AllowAnonymous` 特性即可 ```csharp public class AuthenticationController : Controller { [AllowAnonymous] public IActionResult Login() { return View(); } } ``` ### 2.2、配置認證策略 當然,如果只是這樣修改的話,其實是有問題的,可以看到,當新增上全域性過濾器後,系統已經無法正常的進行訪問 ![啟用授權中介軟體](https://img2020.cnblogs.com/blog/1310859/202101/1310859-20210131171057556-1571046437.png) 對於 authorization(授權) 來說,它其實是在 authentication(認證)通過之後才會進行的操作,也就是說這裡我們缺少了對於系統認證的配置,依據報錯資訊的提示,我們首先需要通過使用 `AddAuthentication` 方法來定義系統的認證策略 ![認證策略](https://img2020.cnblogs.com/blog/1310859/202101/1310859-20210131171112144-1802702885.png) `AddAuthentication` 方法位於 `Microsoft.AspNetCore.Authentication` 類庫中,通過在 Nuget 中搜索就可以發現,.NET Core 已經基於業界通用的規範實現了多個認證策略 因為這裡使用的 Cookie 認證已經包含在預設的專案模板中了,所以就不需要再引用了 ![新增認證服務](https://img2020.cnblogs.com/blog/1310859/202101/1310859-20210131171124848-1728869330.png) 基於 .NET Core 標準的服務使用流程,首先,我們需要在 `Startup.ConfigureServices` 方法來中通過 `AddAuthentication` 來定義整個系統所使用的一個授權策略,以及,基於我們採用 Cookie 授權的方式,結合目前網際網路針對跨站點請求偽造 (CSRF) 攻擊的防範要求,我們需要對網站的 Cookie 進行一些設定 ```csharp public class Startup { public void ConfigureServices(IServiceCollection services) { // 定義授權策略 services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { // 無權訪問的頁路徑 options.AccessDeniedPath = new PathString("/permission/forbidden"); // 登入路徑 options.LoginPath = new PathString("/authentication/login"); // 登出路徑 options.LogoutPath = new PathString("/authentication/logout"); // Cookie 過期時間(20 分鐘) options.ExpireTimeSpan = TimeSpan.FromMinutes(20); }); // 配置 Cookie 策略 services.Configure(options => { // 預設使用者同意非必要的 Cookie options.CheckConsentNeeded = context => true; // 定義 SameSite 策略,Cookies允許與頂級導航一起傳送 options.MinimumSameSitePolicy = SameSiteMode.Lax; }); } } ``` 如程式碼所示,在定義授權策略時,我們定義了三個重定向的頁面,去告訴 Cookie 授權策略這裡對應的頁面在何處,同時,因為身份驗證 Cookie 的預設過期時間會持續到關閉瀏覽器為止,也就是說,只要使用者不點選退出按鈕並且不關閉瀏覽器,使用者會一直處於已經登入的狀態,所以這裡我們設定 20 分鐘的過期時間,避免一些不必要的風險 至此,對於 Cookie 認證策略的配置就完成了,現在就可以在 `Startup.Configure` 方法中新增 `UseAuthentication` 中介軟體到 HTTP 管道中,實現對於網站認證的啟用,這裡需要注意,因為是先認證再授權,所以中介軟體的新增順序不可以顛倒 ```csharp public class Startup { public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); // 新增認證授權(順序不可以顛倒) // app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( "default", "{controller=Home}/{action=Index}/{id?}"); }); } } ``` 此時,當我們再次訪問系統時,因為沒有經過認證,自動觸發了重定向到系統登入頁面的操作,而這裡重定向跳轉的頁面就是上文程式碼中配置的 `LoginPath` 的屬性值 ![登入重定向](https://img2020.cnblogs.com/blog/1310859/202101/1310859-20210131171142449-1601441522.gif) ### 2.3、登入、登出實現 當認證策略配置完成之後,就可以基於選擇的策略來進行登入功能的實現。這裡的登入頁面上的按鈕,模擬了一個登入表單提交,當點選之後會觸發系統的認證邏輯,實現程式碼如下所示。這裡別忘了將登入事件的 Action 上加上 `AllowAnonymous` 特性從而允許匿名訪問 ```csharp [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task LoginAsync() { // 1、Todo:校驗賬戶、密碼是否正確,獲取需要的使用者資訊 // 2、建立使用者宣告資訊 var claims = new List { new Claim(ClaimTypes.Name, "張三"), new Claim(ClaimTypes.MobilePhone, "13912345678") }; // 3、建立宣告身份證 var claimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); // 4、建立宣告身份證的持有者 var claimPrincipal = new ClaimsPrincipal(claimIdentity); // 5、登入 await HttpContext.SignInAsync(claimPrincipal); return Redirect("/"); } ``` 在整塊的程式碼中,涉及到三個主要的物件,`Claim`、`ClaimsIdentity` 和 `ClaimsPrincipal`,通過對於這三個物件的使用,從而實現將使用者登入成功後系統所需的使用者資訊包含在 Cookie 中 三個物件之間的區別,借用[理解ASP.NET Core驗證模型(Claim, ClaimsIdentity, ClaimsPrincipal)不得不讀的英文博文](https://www.cnblogs.com/dudu/p/6367303.html)這篇部落格的解釋來說明 - Claim:被驗證主體特徵的一種表述,比如:登入使用者名稱是...,email是...,使用者Id是...,其中的“登入使用者名稱”,“email”,“使用者Id”就是 ClaimType - ClaimsIdentity:一組 claims 構成了一個 identity,具有這些 claims 的 identity 就是 ClaimsIdentity ,駕照就是一種 ClaimsIdentity,可以把 ClaimsIdentity理解為“證件”,駕照是一種證件,護照也是一種證件 - ClaimsPrincipal:ClaimsIdentity 的持有者就是 ClaimsPrincipal ,一個 ClaimsPrincipal 可以持有多個 ClaimsIdentity,就比如一個人既持有駕照,又持有護照 最後,通過呼叫 `HttpContext.SignInAsync` 方法就可以完成登入功能,可以看到,當 Cookie 被清除後,使用者也就處於登出的狀態了,當然,我們也可以通過手動的呼叫 `HttpContext.SignOutAsync` 來實現登出 ![登入實現](https://img2020.cnblogs.com/blog/1310859/202101/1310859-20210131171202686-907971576.gif) ### 2.4、獲取使用者資訊 對於新增在 Claim 中的資訊,我們可以通過指定 ClaimType 的方式獲取到,在 View 和 Controller 中,我們可以直接通過下面的方式進行獲取,這裡使用到的 User 其實就是上文中提到的 ClaimsPrincipal ```csharp var userName = User.FindFirst(ClaimTypes.Name)?.Value; ``` ![User 物件](https://img2020.cnblogs.com/blog/1310859/202101/1310859-20210131171222452-1250986420.png) 而當我們需要在一個獨立的類庫中獲取儲存的使用者資訊時,我們需要進行如下的操作 第一步,在 `Startup.ConfigureServices` 方法中注入 `HttpContextAccessor` 服務 ```csharp public class Startup { public void ConfigureServices(IServiceCollection services) { // 注入 HttpContext services.AddHttpContextAccessor(); } } ``` 第二步,在你需要使用的類庫中通過 Nuget 引用 `Microsoft.AspNetCore.Http`,之後就可以在具體的類中通過注入 `IHttpContextAccessor` 來獲取到使用者資訊,當然,也可以在此處實現登入、登出的方法 ```csharp namespace Sample.Infrastructure { public interface ICurrentUser { string UserName { get; } Task SignInAsync(ClaimsPrincipal principal); Task SignOutAsync(); Task SignOutAsync(string scheme); } public class CurrentUser : ICurrentUser { private readonly IHttpContextAccessor _httpContextAccessor; private HttpContext HttpContext => _httpContextAccessor.HttpContext; public CurrentUser(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } public string UserName => HttpContext.User.FindFirst(ClaimTypes.Name)?.Value; public Task SignInAsync(ClaimsPrincipal principal) => HttpContext.SignInAsync(principal); public Task SignOutAsync() => HttpContext.SignOutAsync(); public Task SignOutAsync(string scheme) => HttpContext.SignOutAsync(scheme); } } ``` 至此,整塊的認證功能就已經實現了,希望對你有所幫助 ## Reference 1. [SameSite cookies](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie/SameSite) 2. [Work with SameSite cookies in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-5.0) 3. [What does the CookieAuthenticationOptions.LogoutPath property do in ASP.NET Core 2.1?](https://stackoverflow.com/questions/52709492/what-does-the-cookieauthenticationoptions-logoutpath-property-do-in-asp-net-core) 4. [理解ASP.NET Core驗證模型(Claim, ClaimsIdentity, ClaimsPrincipal)不得不讀的英文博文](https://www.cnblogs.com/dudu/p/6367303.html) 5. [Introduction to Authentication with ASP.NET Core](https://andrewlock.net/introduction-to-authentication-with-asp-net