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
添加中間件即可
[Authorize]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
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
過濾器,實現對於全局的認證管控
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews()
.AddMvcOptions(options => { options.Filters.Add(new AuthorizeFilter()); });
}
}
此時,對於一些不需要進行認證就可以訪問的頁面,只需要添加 AllowAnonymous
特性即可
public class AuthenticationController : Controller
{
[AllowAnonymous]
public IActionResult Login()
{
return View();
}
}
2.2、配置認證策略
當然,如果只是這樣修改的話,其實是有問題的,可以看到,當添加上全局過濾器後,系統已經無法正常的進行訪問
對於 authorization(授權) 來說,它其實是在 authentication(認證)通過之後纔會進行的操作,也就是說這裏我們缺少了對於系統認證的配置,依據報錯信息的提示,我們首先需要通過使用 AddAuthentication
方法來定義系統的認證策略
AddAuthentication
方法位於 Microsoft.AspNetCore.Authentication
類庫中,通過在 Nuget 中搜索就可以發現,.NET Core 已經基於業界通用的規範實現了多個認證策略
因爲這裏使用的 Cookie 認證已經包含在默認的項目模板中了,所以就不需要再引用了
基於 .NET Core 標準的服務使用流程,首先,我們需要在 Startup.ConfigureServices
方法來中通過 AddAuthentication
來定義整個系統所使用的一個授權策略,以及,基於我們採用 Cookie 授權的方式,結合目前互聯網針對跨站點請求僞造 (CSRF) 攻擊的防範要求,我們需要對網站的 Cookie 進行一些設定
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<CookiePolicyOptions>(options =>
{
// 默認用戶同意非必要的 Cookie
options.CheckConsentNeeded = context => true;
// 定義 SameSite 策略,Cookies允許與頂級導航一起發送
options.MinimumSameSitePolicy = SameSiteMode.Lax;
});
}
}
如代碼所示,在定義授權策略時,我們定義了三個重定向的頁面,去告訴 Cookie 授權策略這裏對應的頁面在何處,同時,因爲身份驗證 Cookie 的默認過期時間會持續到關閉瀏覽器爲止,也就是說,只要用戶不點擊退出按鈕並且不關閉瀏覽器,用戶會一直處於已經登錄的狀態,所以這裏我們設定 20 分鐘的過期時間,避免一些不必要的風險
至此,對於 Cookie 認證策略的配置就完成了,現在就可以在 Startup.Configure
方法中添加 UseAuthentication
中間件到 HTTP 管道中,實現對於網站認證的啓用,這裏需要注意,因爲是先認證再授權,所以中間件的添加順序不可以顛倒
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
的屬性值
2.3、登錄、登出實現
當認證策略配置完成之後,就可以基於選擇的策略來進行登錄功能的實現。這裏的登錄頁面上的按鈕,模擬了一個登錄表單提交,當點擊之後會觸發系統的認證邏輯,實現代碼如下所示。這裏別忘了將登錄事件的 Action 上加上 AllowAnonymous
特性從而允許匿名訪問
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginAsync()
{
// 1、Todo:校驗賬戶、密碼是否正確,獲取需要的用戶信息
// 2、創建用戶聲明信息
var claims = new List<Claim>
{
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)不得不讀的英文博文這篇博客的解釋來說明
- Claim:被驗證主體特徵的一種表述,比如:登錄用戶名是...,email是...,用戶Id是...,其中的“登錄用戶名”,“email”,“用戶Id”就是 ClaimType
- ClaimsIdentity:一組 claims 構成了一個 identity,具有這些 claims 的 identity 就是 ClaimsIdentity ,駕照就是一種 ClaimsIdentity,可以把 ClaimsIdentity理解爲“證件”,駕照是一種證件,護照也是一種證件
- ClaimsPrincipal:ClaimsIdentity 的持有者就是 ClaimsPrincipal ,一個 ClaimsPrincipal 可以持有多個 ClaimsIdentity,就比如一個人既持有駕照,又持有護照
最後,通過調用 HttpContext.SignInAsync
方法就可以完成登錄功能,可以看到,當 Cookie 被清除後,用戶也就處於登出的狀態了,當然,我們也可以通過手動的調用 HttpContext.SignOutAsync
來實現登出
2.4、獲取用戶信息
對於添加在 Claim 中的信息,我們可以通過指定 ClaimType 的方式獲取到,在 View 和 Controller 中,我們可以直接通過下面的方式進行獲取,這裏使用到的 User 其實就是上文中提到的 ClaimsPrincipal
var userName = User.FindFirst(ClaimTypes.Name)?.Value;
而當我們需要在一個獨立的類庫中獲取存儲的用戶信息時,我們需要進行如下的操作
第一步,在 Startup.ConfigureServices
方法中注入 HttpContextAccessor
服務
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// 注入 HttpContext
services.AddHttpContextAccessor();
}
}
第二步,在你需要使用的類庫中通過 Nuget 引用 Microsoft.AspNetCore.Http
,之後就可以在具體的類中通過注入 IHttpContextAccessor
來獲取到用戶信息,當然,也可以在此處實現登錄、登出的方法
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);
}
}
至此,整塊的認證功能就已經實現了,希望對你有所幫助