在 ASP.NET Core 應用中使用 Cookie 進行身份認證
阿新 • • 發佈:2021-02-01
## 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