自訂.NET Core Identity身份驗證和授權教學: Cookie SignIn 自訂.NET Core Identity身份驗證和授權教學:Cookie SignIn

Published on Sunday, April 16, 2023

至今為止我們已經了解 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. 轉跳回原網址

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

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

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

學習後回到我們自己創的專案上手動註冊看看

builder.Services
    .AddAuthentication()
    .AddCookie(IdentityConstants.ApplicationScheme,options =>
    {
        options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
    });
    
builder.Services.AddIdentityCore<IdentityUser>()
    .AddUserStore<CustomUserStore>()
    .AddSignInManager<SignInManager<IdentityUser>>();
    
app.UseAuthentication();

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 界面,使用 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));
}

新增完 CustomStore 之後需要重新建立 User,因為我們之前並沒有實現 IUserPasswordStore 界面,所以 User 並沒有密碼

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

之後查詢 User 會發現 passwordHash 會自動對密碼加密

Response body
{
  "id": "d319d7eb-140a-4958-bb0a-cd619b400942",
  "userName": "User1",
  "normalizedUserName": "USER1",
  "email": null,
  "normalizedEmail": null,
  "emailConfirmed": false,
  "passwordHash": "AQAAAAIAAYagAAAAEKEMP1C8dEb0bw19IUkDjNDnLB3ZhE2ByhWIrLIDV1RraByQ+JPRG0+nrhNiVCCPzg==",
  "securityStamp": "0a442f54-5f5f-48df-8a3b-9da39621a565",
  "concurrencyStamp": "0929275f-0180-44f9-9946-6a104e44d90c",
  "phoneNumber": null,
  "phoneNumberConfirmed": false,
  "twoFactorEnabled": false,
  "lockoutEnd": null,
  "lockoutEnabled": false,
  "accessFailedCount": 0
}

完成之後使用我們新的 API 並且輸入帳號密碼,執行後會回傳以下內容

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

看到 "succeeded": true 就代表登入成功了,此時可以檢查瀏覽器的 Cookie
域名底下會發現多了一個 Cookie .AspNetCore.Identity.Application 我們在 WeatherForecastController 新增驗證,需要登入才可以使用此API

[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase

Summary

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

今天的進度 Github