如何實作沒有 ASP.NET Core Identity 的 Cookie-based 身分驗證機制 | The Will Will Web

The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

如何實作沒有 ASP.NET Core Identity 的 Cookie-based 身分驗證機制

在 .NET Framework 中,身分認證機制可以使用「表單驗證」(Forms Authentication) 來實作,這種以 Cookie 為基礎的身分驗證方式,相當容易上手,只要學習幾個 API 就可以快速完成實作。本篇文章我將解說如何在 ASP․NET Core 中實作出類似「表單驗證」的身分驗證機制,雖然架構相似,但名稱已改,建議各位日後就稱他為「以 Cookie 為基礎的身分驗證方式」,英文是「 Cookie-based Authentication 」!

回顧過往表單驗證的設定方法

如果你想參考過往 .NET Framework 時期的表單驗證方式,可以直接參考 Explained: Forms Authentication in ASP.NET 2.0 這篇文章,其觀念、解說、範例一應俱全,值得一看。

我將其設定方法歸類為以下三個步驟:

  1. 設定 IIS Authentication

    自從 ASP․NET 2.0 開始,就內建一個 FormsAuthenticationModule HTTP 模組,主要用來負責「表單驗證」之用,這裡的設定由於跟 IIS 相關,所以相關設定當然是寫在 web.config 檔案中,以下是設定範例:

    <system.web>
        <authentication mode="Forms">
            <forms loginUrl="Login.aspx"
                protection="All"
                timeout="30"
                name=".ASPXAUTH"
                path="/"
                requireSSL="false"
                slidingExpiration="true"
                defaultUrl="default.aspx"
                cookieless="UseDeviceProfile"
                enableCrossAppRedirects="false" />
        </authentication>
    </system.web>
    

    請注意:ASP․NET 預設就會啟用 FormsAuthenticationModule 模組,但是 Visual Studio 內建的 ASP.NET Web API 2 專案範本中,其 web.config 有特別將該模組移除,所以如果想在 ASP․NET Web API 2 中使用的話,請參考 如何在 ASP.NET Web API 2 專案中啟用表單驗證 (Forms Authentication) 文章進行設定。

  2. 調整網頁授權規則

    IIS 預設允許以匿名存取網頁,因此你必須特別設定授權規則才能限制使用者存取網站,以下是設定範例:

    <system.web>
        <authorization>
            <deny users="?" />
        </authorization>
    </system.web>
    
  3. 撰寫登入、登出、授權等程式碼

    這部分請參考 Explained: Forms Authentication in ASP.NET 2.0 文章說明。

ASP․NET Core 使用以 Cookie 為基礎的身分驗證方式

我們都知道 ASP․NET Core 的年代並沒有 web.config 設定檔,跟 IIS 也沒有直接的相依性,因此很多設定都變了。以下是設定 Cookie-based 驗證的步驟:

  1. 調整 Startup.ConfigureServices 方法

    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie();
    

    這裡的 AddAuthentication() 主要是加入 Authentication Middleware 這個中間層,這將會告知 ASP․NET Core 採用身分認證機制。而 AddCookie() 則是告知 ASP․NET Core 我們將使用 Cookie 驗證機制。

    其中 CookieAuthenticationDefaults.AuthenticationScheme 其實只是一個字串 Cookies 而已,所以其實你也可以這樣寫 (不建議這樣寫):

    services.AddAuthentication("Cookies").AddCookie();
    

    關於 ASP․NET Core 對於 Cookie 的身分驗證方式,所有的預設值都被定義在 CookieAuthenticationDefaults.cs 原始碼中,直接看原始碼應該可以獲得最完整的資訊。如果想調整預設值,可以參考以下語法進行調整:(請參考 CookieAuthenticationOptions 類別)

    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(options =>
        {
            ...
        });
    
  2. 調整 Startup.Configure 方法

    app.UseAuthentication();
    

    請務必設定在 app.UseAuthorization(); 之前。

    app.UseAuthentication();app.UseAuthorization(); 必須放置在 app.UseEndpoints(); 之前。

登入實作說明

要實作以 Cookie 為基礎的身分驗證,首要任務就是建立一個含有使用者認證資訊的 Cookie 物件,並傳送到用戶端瀏覽器。

var user = new
{
    Email = "will@example.com",
    FullName = "Will Huang"
};

var claims = new List<Claim>
{
    new Claim(ClaimTypes.Name, user.Email),
    new Claim("FullName", user.FullName),
    new Claim(ClaimTypes.Role, "Administrator"),
};

var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);

await HttpContext.SignInAsync(new ClaimsPrincipal(claimsIdentity));

上述程式的 HttpContext.SignInAsync() 的主要目的是將準備好的 ClaimsPrincipal 物件送回到用戶端瀏覽器。然而 ClaimsPrincipal 物件來自於 ClaimsIdentity 物件,你必須準備好所有的聲明資訊(Claims),而這些 Claims 是讓 ASP․NET Core 認識你是誰的重要關鍵,尤其是 ClaimTypes.NameClaimTypes.Role 這兩個 Claims 都必須設定。

由於 ASP․NET Core 使用 Data Protection 系統來對 Cookie 內容進行加解密。如果你的網站想在多台電腦之間做負載平衡,你必須設定妥善設定每台電腦之間的金鑰,確保 Cookie 在每一台電腦都能解密。詳情請參閱 Configure ASP.NET Core Data Protection 文件。

關於 Cookie 身分認證的安全性

由於 HTTP 屬於無狀態的通訊協定,我們為了要從後端識別前端用戶的身分 (Identity),最重要的地方,就是有一個可以識別用戶的資訊來源。當你使用以 Cookie 為基礎的身分驗證方式時,這個 Cookie 就是你唯一的識別來源 (the single source of identity)。因此,只要 Cookie 本身是合法的,系統就會自動視為這是一個「已經通過身分驗證」的使用者,所以我們的首要任務,就是「確保」這份 Cookie 不會被使用者任意偽造!

就因為用戶端所有的資訊都是有可能偽造的,即便你設定了 Cookie 的到期時間,駭客還是有機會將 Cookie 儲存下來,輕易的就可以讓 Cookie 持續的使用下去。因此,當你想從後端對已經通過身分驗證的使用者進行管理,例如你想立即停用特定使用者的使用權限,光是依賴 Cookie 的到期時間是非常不可靠的,你應該要能做到,即便 Cookie 是有效的,也不能再讓他使用才對。

ASP․NET Core 內建的 Cookies 身分驗證機制,只有做到「確保」這份 Cookie 不會被使用者任意偽造,因為 Cookie 的內容是加密過的,必須要有金鑰才能解密,且只有能成功解密的 Cookie 才會被視為合法的已登入使用者。但若使用者將 Cookie 偷偷存下來,或是任意竄改瀏覽器 Cookie 的到期日,那麼這個人就有可能永遠不需要再次登入了,因為這份 Cookie 永遠是有效的。也因為這樣,我們通常會將 Cookie 驗證方式視為「相對不安全」的認證方法。

不過 ASP․NET Core 可以透過擴充 CookieAuthenticationEvents 的方式,做到自定的 Cookie 有效性驗證邏輯。例如你可以加入一個時間戳記到 Cookie 中,並透過自定的 ValidatePrincipal 事件來檢查 Cookie 是否有效,如此一來,就可以再提升 Cookie 身分驗證的安全性。相關做法請參見 React to back-end changes 文件說明。

設定 Cookie 的類型與存活時間

瀏覽器 Cookie 有區分兩種類型:

  1. Session Cookie (瀏覽器關閉後 Cookie 就會自動刪除)
  2. Persistent cookies (瀏覽器關閉後 Cookie 還會存留一段時間)

ASP․NET Core 預設採用 Session Cookie 機制。但如果你希望讓使用者登入時可以記憶登入狀態一段時間,你可以在登入的時候改用以下寫法:

var authProperties = new AuthenticationProperties
{
    //AllowRefresh = <bool>,
    // Refreshing the authentication session should be allowed.

    ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
    // The time at which the authentication ticket expires. A
    // value set here overrides the ExpireTimeSpan option of
    // CookieAuthenticationOptions set with AddCookie.

    IsPersistent = true,
    // Whether the authentication session is persisted across
    // multiple requests. When used with cookies, controls
    // whether the cookie's lifetime is absolute (matching the
    // lifetime of the authentication ticket) or session-based.

    //IssuedUtc = <DateTimeOffset>,
    // The time at which the authentication ticket was issued.

    //RedirectUri = <string>
    // The full path or absolute URI to be used as an http
    // redirect response value.
};

await HttpContext.SignInAsync(new ClaimsPrincipal(claimsIdentity), authProperties);

上述範例程式,尤其是 ExpiresUtc 要特別注意,你必須使用 UTC 時區為主,千萬不要弄錯囉。

設定 API 或 MVC 授權

這部分跟之前幾乎一樣,只要在 Controller 或 Action 上面套用 [Authorize] 即可。

不過,當你在尚未登入尚未取得角色授權的情況下進入某個套用 [Authorize] 的頁面 (Action),預設將會自動導向到登入頁面,而 ASP․NET Core 使用 Cookie 身分驗證時,預設的登入網址為 /Account/Login,如果你想要調整預設登入位址,可以在 Startup.ConfigureServices 設定 CookieAuthenticationOptions.LoginPath 屬性即可,如下範例:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options => {
        options.LoginPath = new PathString("/Signin");
    });

事實上,當你嘗試直接連接到特定一個受限制的網頁時 (需要登入才能看到的網頁),預設不但會轉向到 /Account/Login 網址,他預設還會代入一個 Query String 名為 ReturnUrl,他會放上你原本想存取的網址,方便你在登入成功後,可以設定轉向到使用者原本想進入的頁面,這樣的設計可以增加用戶體驗(UX)。網址範例如下:

https://localhost:5001/Account/Login?ReturnUrl=%2FHome%2FPrivacy

如果你想調整這個 Query String 參數名稱,也可以在 Startup.ConfigureServices 設定 CookieAuthenticationOptions.ReturnUrlParameter 屬性,如下範例:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options => {
        options.LoginPath = new PathString("/Signin");
        options.ReturnUrlParameter = "ret";
    });

所有選項預設值請參考 CookieAuthenticationDefaults.cs 原始碼。

登出實作說明

登出的實作跟 ASP․NET 的 Forms Authentication 一樣簡單,一行就能解決:

await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

相關連結