在上一篇的文章 我們已經了解 UserStoreUserManager 的使用場景,今天來學習 Identity 中驗證部份的邏輯 在上一篇的文章中有稍微提到 SignInManager 如名稱所示,我們可以猜到這個 Manager 是負責處理登入這部份的商業邏輯 閱讀原始碼之後發現 SignInManager 內部也有注入 UserManager方法,代表 SignInManager 也只更高階的封裝 Github

至於該怎麼使用可以先參考 Microsoft.AspNetCore.Identity.UI 這個 Package 之前有提到裡面有預設的 Razor 頁面可以直接供我們參考 Github 此 Login 頁面中的 OnPostAsync 方法,會在我們按下 Button 後執行

  1. 使用 SignInManager.GetExternalAuthenticationSchemesAsync 檢查系統之中使否有設定第三方登入(Google),有的話就在底下顯示按鈕
  2. 使用 SignInManager.PasswordSignInAsync 輸入帳號密碼登入
  3. 檢查登入回傳結果與是否有開啟二次驗證
  4. 轉跳回原網址

得知了 SignInManager 不只支援一般的帳號密碼登入同時也支援第三方登入, 那目前的任務就是查看微軟官方提供了多少種登入方式給我們直接使用,我們可以在 Github 上面找到 發現微軟提供了 CookiesJwtBearer 等常用的驗證方式,與 Google、Twitter、Facebook 等等外部登入驗證。

那麼該怎麼告訴系統我們想要使用 Cookies 進行驗證呢? 先參考 Github 上的 Sample 學習一下該如何進行註冊

  1. 使用 AddAuthentication 方法將 Cookie AuthenticationScheme 註冊到 Services 之中
  2. 使用 AddCookie 方法設定 Cookie 的過期時間與更新規則
  3. 使用 UseAuthentication 方法指定流程需要使用 AuthenticationMiddleware

學習後回到我們自己的專案上手動註冊試試,為了方便測試這裡把 RequireConfirmedAccount 移除了,否則就需要實做 IUserEmailStore<IdentityUser> 介面。

builder.Services
    .AddAuthentication()
    .AddCookie(IdentityConstants.ApplicationScheme,options =>
    {
        options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
    });

builder.Services.AddIdentityCore<IdentityUser>()
    .AddUserStore<CustomUserStore>()
    .AddSignInManager<SignInManager<IdentityUser>>();
    
app.UseAuthentication();
app.UseAuthorization();

AccountController 添加新方法

[HttpPost(template: "~/signin", Name = "SignIn")]
public async Task<SignInResult> SignIn(string userName, string password)
{
    return await _signInManager.PasswordSignInAsync(
        userName, 
        password, 
        false, 
        false);
}

在測試之前需要在我們的 CustomUserStore 實現 IUserPasswordStore 界面,因為 SignInManager 內部需要設定密碼 Hash,使用 EFCore 可以跳過這步驟

public Task SetPasswordHashAsync(IdentityUser user, string? passwordHash, CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();
    if (user == null)
    {
        throw new ArgumentNullException(nameof(user), $"Parameter {nameof(user)} cannot be null.");
    }
    if (passwordHash == null)
    {
        throw new ArgumentNullException(nameof(passwordHash), $"Parameter {nameof(passwordHash)} cannot be null.");
    }
    user.PasswordHash = passwordHash;
    return Task.CompletedTask;
}

public Task<string?> GetPasswordHashAsync(IdentityUser user, CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();
    if (user == null)
    {
        throw new ArgumentNullException(nameof(user), $"Parameter {nameof(user)} cannot be null.");
    }
    return Task.FromResult(user.PasswordHash);
}

public Task<bool> HasPasswordAsync(IdentityUser user, CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();
    if (user == null)
    {
        throw new ArgumentNullException(nameof(user), $"Parameter {nameof(user)} cannot be null.");
    }
    return Task.FromResult(!string.IsNullOrEmpty(user.PasswordHash));
}

由於之前建立的 User 都是沒有密碼的,所以回到 AccountController 在註冊時加上密碼參數。

[HttpPost("CreateUser")]
public async Task<IdentityResult> CreateUser(string userName, string password)
{
    var identityUser = new IdentityUser(userName);
    return await _userManager.CreateAsync(identityUser, password);
}

接下來 Post https://localhost:7057/Account/CreateUser?userName=user3&password=1q2w3E* 建立 user3 會員

查詢會發現 passwordHash 會在 UserManager.CreateAsync 執行的時候幫你設定好

{
    "id": "5ad20e10-554d-45c4-842a-30e6ba4f65d7",
    "userName": "user3",
    "normalizedUserName": "USER3",
    "email": null,
    "normalizedEmail": null,
    "emailConfirmed": false,
    "passwordHash": "AQAAAAIAAYagAAAAEH0wEDNdxTqAnamzFKBRjW1fXrJxDeRMQnDnFVdcnoffz/uoeodhXosLAcFQtqRFGw==",
    "securityStamp": "c22286d1-d45d-44fa-a76b-159771fe5d09",
    "concurrencyStamp": "31c7bd93-4877-4fd6-aa6d-d839700817cf",
    "phoneNumber": null,
    "phoneNumberConfirmed": false,
    "twoFactorEnabled": false,
    "lockoutEnd": null,
    "lockoutEnabled": false,
    "accessFailedCount": 0
}

完成之後 Post 新的 API https://localhost:7057/Account/SignIn?userName=user3&password=1q2w3E* 進行登入,執行後會回傳以下內容

Response body
{
  "succeeded": true,
  "isLockedOut": false,
  "isNotAllowed": false,
  "requiresTwoFactor": false
}

看到 "succeeded": true 就代表登入成功了,此時可以檢查瀏覽器的 Cookie
localhost 域名底下會發現多了一個 Cookie .AspNetCore.Identity.Application,這個 Cookie 就是 dotnet 預設的登入 Cookie 名稱

我們在 GetUser 方法新增驗證,代表需要登入才能取得會員詳細資訊

[Authorize]
[HttpGet("GetUser")]
public async Task<IdentityUser?> GetUser(string userName)
{
    return await _userManager.FindByNameAsync(userName);
}

會發現沒登入直接呼叫會回傳錯誤,只有先登入後呼叫查詢才會正常回傳資訊


Summary

今天學習了 SignInManager 與使用 Cookie 來進行登入驗證,並且知道微軟已經內建提供了許多登入方式(Schema) 之後的文章會繼續試試看 JWT 或者第三方登入。