﻿# 如何實作沒有 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](https://learn.microsoft.com/en-us/previous-versions/msp-n-p/ff647070\(v=pandp.10\)) 這篇文章，其觀念、解說、範例一應俱全，值得一看。

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

1.  設定 [IIS Authentication](https://learn.microsoft.com/en-us/previous-versions/msp-n-p/ff647070\(v=pandp.10\)#iis-authentication)
    
    自從 ASP․NET 2.0 開始，就內建一個 `FormsAuthenticationModule` HTTP 模組，主要用來負責「表單驗證」之用，這裡的設定由於跟 IIS 相關，所以相關設定當然是寫在 `web.config` 檔案中，以下是設定範例：
    
    ```xml
    <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)](/post/2016/09/22/Enable-FormsAuthentication-in-ASPNET-Web-API-2) 文章進行設定。
    
2.  調整網頁授權規則
    
    IIS 預設允許以匿名存取網頁，因此你必須特別設定授權規則才能限制使用者存取網站，以下是設定範例：
    
    ```xml
    <system.web>
        <authorization>
            <deny users="?" />
        </authorization>
    </system.web>
    ```
    
3.  撰寫登入、登出、授權等程式碼
    
    這部分請參考 [Explained: Forms Authentication in ASP.NET 2.0](https://learn.microsoft.com/en-us/previous-versions/msp-n-p/ff647070\(v=pandp.10\)) 文章說明。
    

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

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

1.  調整 `Startup.ConfigureServices` 方法
    
    ```csharp
    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie();
    services.AddAuthorization();
    ```
    
    這裡的 `AddAuthentication()` 主要是加入 `Authentication Middleware` 這個中間層，這將會告知 ASP․NET Core 採用身分認證機制。而 `AddCookie()` 則是告知 ASP․NET Core 我們將使用 Cookie 驗證機制。
    
    其中 [CookieAuthenticationDefaults.AuthenticationScheme](https://github.com/aspnet/AspNetCore/blob/release/3.1/src/Security/Authentication/Cookies/src/CookieAuthenticationDefaults.cs#L16) 其實只是一個字串 `Cookies` 而已，所以其實你也可以這樣寫 (不建議這樣寫)：
    
    ```csharp
    services.AddAuthentication("Cookies").AddCookie();
    services.AddAuthorization();
    ```
    
    關於 ASP․NET Core 對於 Cookie 的身分驗證方式，所有的預設值都被定義在 [CookieAuthenticationDefaults.cs](https://github.com/aspnet/AspNetCore/blob/release/3.1/src/Security/Authentication/Cookies/src/CookieAuthenticationDefaults.cs) 原始碼中，直接看原始碼應該可以獲得最完整的資訊。如果想調整預設值，可以參考以下語法進行調整：(請參考 [CookieAuthenticationOptions](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions) 類別)
    
    ```csharp
    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(options =>
        {
            options.LoginPath = "/signin";
            ...
        });
    services.AddAuthorization();
    ```
    
2.  調整 `Startup.Configure` 方法
    
    ```csharp
    app.UseAuthentication();
    ```
    
    > 請務必設定在 `app.UseAuthorization();` 之前。
    
    > `app.UseAuthentication();` 與 `app.UseAuthorization();` 必須放置在 `app.UseEndpoints();` 之前。
    

### 登入實作說明

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

```csharp
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.Name` 與 `ClaimTypes.Role` 這兩個 Claims 建議都做設定。例如你在 Controller 中使用 `User.Identity.Name` 的話，他預設會讀取的 `ClaimTypes.Name` 內容。而當你在 `[Authorize(roles = "Admin")]` 使用 `roles` 屬性的話，就會自動讀取 `ClaimTypes.Role` 的內容。當然，你不設定這兩個 Claims 也是可以的！

> 由於 ASP․NET Core 使用 [Data Protection](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/using-data-protection?view=aspnetcore-3.1) 系統來對 Cookie 內容進行加解密。如果你的網站想在多台電腦之間做負載平衡，你必須設定妥善設定每台電腦之間的金鑰，確保 Cookie 在每一台電腦都能解密。詳情請參閱 [Configure ASP.NET Core Data Protection](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-3.1) 文件。

### 關於 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 驗證方式視為「相對不安全」的認證方法。

不過 ASP․NET Core 可以透過擴充 [CookieAuthenticationEvents](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationevents?view=aspnetcore-3.1) 的方式，做到自定的驗證邏輯。例如你可以加入一個時間戳記到 Cookie 中，並透過自定的 [ValidatePrincipal](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationevents.validateprincipal) 事件來檢查 Cookie 是否有效，如此一來，就可以再提升 Cookie 身分驗證的安全性。相關做法請參見 [React to back-end changes](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1#react-to-back-end-changes) 文件說明。

### 設定 Cookie 的類型與存活時間

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

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

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

```csharp
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` 屬性即可，如下範例：

```csharp
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` 屬性，如下範例：

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

> 所有選項預設值請參考 [CookieAuthenticationDefaults.cs](https://github.com/aspnet/AspNetCore/blob/release/3.1/src/Security/Authentication/Cookies/src/CookieAuthenticationDefaults.cs) 原始碼。

### 登出實作說明

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

```csharp
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
```

### 相關連結

-   [Use cookie authentication without ASP.NET Core Identity | Microsoft Docs](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1)
-   [\[ASP.net Core 3.0\] 實作 Cookie based 登入機制(CookieAuthentication) 取代ASP.Net Framework的表單驗證(FormsAuthentication)登入 | 高級打字員的技術雲 - 點部落](https://dotblogs.com.tw/shadow/2019/01/16/105615)
