.NET8 Identity Register

YataoFeng發表於2024-05-22

分享給需要幫助的人:記一次 IdentityAPI 中註冊的原始碼解讀,為什麼有這篇文? 因為當我看到原始碼時,發現它的邏輯竟然是固定死的。我們並不是只能按照微軟提供的原始碼去做。此文內容包含:設定使用者賬戶為未驗證狀態延遲使用者建立、優缺點的說明、適用場景。


在ASP.NET 8 Identity 中註冊API的原始碼如下:

routeGroup.MapPost("/register", async Task<Results<Ok, ValidationProblem>>
    ([FromBody] RegisterRequest registration, HttpContext context, [FromServices] IServiceProvider sp) =>
{
    var userManager = sp.GetRequiredService<UserManager<TUser>>();

    if (!userManager.SupportsUserEmail)
    {
        throw new NotSupportedException($"{nameof(MapIdentityApi)} requires a user store with email support.");
    }

    var userStore = sp.GetRequiredService<IUserStore<TUser>>();
    var emailStore = (IUserEmailStore<TUser>)userStore;
    var email = registration.Email;

    if (string.IsNullOrEmpty(email) || !_emailAddressAttribute.IsValid(email))
    {
        return CreateValidationProblem(IdentityResult.Failed(userManager.ErrorDescriber.InvalidEmail(email)));
    }

    var user = new TUser { EmailConfirmed = false }; // 標記為未驗證
    await userStore.SetUserNameAsync(user, email, CancellationToken.None);
    await emailStore.SetEmailAsync(user, email, CancellationToken.None);
    var result = await userManager.CreateAsync(user, registration.Password);

    if (!result.Succeeded)
    {
        return CreateValidationProblem(result);
    }

    await SendConfirmationEmailAsync(user, userManager, context, email);
    return TypedResults.Ok();
});

routeGroup.MapGet("/confirm-email", async Task<IResult>
    ([FromQuery] string userId, [FromQuery] string token, [FromServices] UserManager<TUser> userManager) =>
{
    var user = await userManager.FindByIdAsync(userId);
    if (user == null)
    {
        return TypedResults.BadRequest("Invalid user.");
    }

    var result = await userManager.ConfirmEmailAsync(user, token);
    if (!result.Succeeded)
    {
        return TypedResults.BadRequest("Email confirmation failed.");
    }

    user.EmailConfirmed = true; // 更新為已驗證
    await userManager.UpdateAsync(user);

    return TypedResults.Ok("Email confirmed successfully.");
});

會發現它在註冊的時候使用郵箱作為使用者名稱,配置了郵箱和密碼。但是它在傳送郵箱驗證碼之前,就已經透過CreateAsync建立好了賬號。這種方式叫做設定使用者賬戶為未驗證狀態,將 EmailConfirmed 設定為 false,郵箱驗證確認後設定為true。
這種方式的缺點很明顯:

  1. 資料庫冗餘:未驗證的使用者仍然會被建立並儲存在資料庫中,可能會增加垃圾資料。
  2. 風險較高:未驗證使用者在短時間內可能會嘗試惡意行為,需要額外的監控和限制措施。

優點如下:

  1. 實現簡單:直接在使用者建立時標記使用者為未驗證,邏輯簡單易於實現。
  2. 使用者體驗:使用者可以立即註冊並部分使用系統功能,驗證郵箱可以稍後進行。
  3. 安全可控:透過限制未驗證使用者的操作,可以在確保安全性的同時提供基本的使用者體驗。

更安全的方式是延遲使用者建立,程式碼如下:

routeGroup.MapPost("/register", async Task<IResult>
    ([FromBody] RegisterRequest registration, HttpContext context, [FromServices] IServiceProvider sp) =>
{
    var userManager = sp.GetRequiredService<UserManager<TUser>>();

    if (!userManager.SupportsUserEmail)
    {
        throw new NotSupportedException($"{nameof(MapIdentityApi)} requires a user store with email support.");
    }

    var userStore = sp.GetRequiredService<IUserStore<TUser>>();
    var emailStore = (IUserEmailStore<TUser>)userStore;
    var email = registration.Email;

    if (string.IsNullOrEmpty(email) || !_emailAddressAttribute.IsValid(email))
    {
        return CreateValidationProblem(IdentityResult.Failed(userManager.ErrorDescriber.InvalidEmail(email)));
    }

    // 生成驗證令牌併傳送確認郵件
    var verificationToken = GenerateVerificationToken();
    await SendVerificationEmailAsync(email, verificationToken, context);

    // 臨時儲存註冊資訊
    SaveTemporaryRegistrationInfo(registration, verificationToken);

    return TypedResults.Ok("Please confirm your email.");
});

routeGroup.MapGet("/confirm-email", async Task<IResult>
    ([FromQuery] string token, [FromServices] IServiceProvider sp) =>
{
    var registration = GetTemporaryRegistrationInfoByToken(token);

    if (registration == null)
    {
        return TypedResults.BadRequest("Invalid or expired token.");
    }

    var userManager = sp.GetRequiredService<UserManager<TUser>>();
    var user = new TUser();
    await userStore.SetUserNameAsync(user, registration.Email, CancellationToken.None);
    await emailStore.SetEmailAsync(user, registration.Email, CancellationToken.None);
    var result = await userManager.CreateAsync(user, registration.Password);

    if (!result.Succeeded)
    {
        return CreateValidationProblem(result);
    }

    return TypedResults.Ok("Email confirmed and user created.");
});

會發現它與第一個例子是相反的,它是使用者註冊後把資料儲存在了臨時的記憶體中,再向郵箱傳送驗證碼。透過配置郵箱的時候,用驗證碼得到使用者資料,並以此建立新的賬號。

此做法的缺點也很明顯:

  1. 實現複雜:需要額外的邏輯來儲存臨時註冊資訊並處理驗證令牌。
  2. 使用者體驗:使用者在註冊後需要先驗證郵箱才能完成註冊流程,可能會導致部分使用者流失。

優點如下:

  1. 避免垃圾使用者:只有當使用者驗證了郵箱後,才會正式建立使用者賬戶,減少垃圾使用者數量。
  2. 安全性高:在使用者點選確認連結前,賬戶資訊不會進入資料庫,降低被濫用的風險。
  3. 資源節省:避免建立大量未驗證的使用者,節省資料庫儲存和處理資源。

它們的適用場景如下:

  1. 延遲使用者建立:適用於希望最大限度減少垃圾使用者並確保使用者郵箱有效性的場景,如高安全性要求的系統。
  2. 設定使用者賬戶為未驗證狀態:適用於希望提供更流暢的使用者體驗,允許使用者在驗證郵箱前進行部分操作的場景,如社交平臺或內容網站。

相關文章