在 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 添加中間件即可

[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("/");
}

在整塊的代碼中,涉及到三個主要的對象,ClaimClaimsIdentityClaimsPrincipal,通過對於這三個對象的使用,從而實現將用戶登錄成功後系統所需的用戶信息包含在 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;

User 對象

而當我們需要在一個獨立的類庫中獲取存儲的用戶信息時,我們需要進行如下的操作

第一步,在 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);
    }
}

至此,整塊的認證功能就已經實現了,希望對你有所幫助

Reference

  1. SameSite cookies
  2. Work with SameSite cookies in ASP.NET Core
  3. What does the CookieAuthenticationOptions.LogoutPath property do in ASP.NET Core 2.1?
  4. 理解ASP.NET Core驗證模型(Claim, ClaimsIdentity, ClaimsPrincipal)不得不讀的英文博文
  5. Introduction to Authentication with ASP.NET Core
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章