• .NET8 Identity Register


    分享给需要帮助的人:记一次 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>();
    
        if (!userManager.SupportsUserEmail)
        {
            throw new NotSupportedException($"{nameof(MapIdentityApi)} requires a user store with email support.");
        }
    
        var userStore = sp.GetRequiredService>();
        var emailStore = (IUserEmailStore)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 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>();
    
        if (!userManager.SupportsUserEmail)
        {
            throw new NotSupportedException($"{nameof(MapIdentityApi)} requires a user store with email support.");
        }
    
        var userStore = sp.GetRequiredService>();
        var emailStore = (IUserEmailStore)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>();
        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. 设置用户账户为未验证状态:适用于希望提供更流畅的用户体验,允许用户在验证邮箱前进行部分操作的场景,如社交平台或内容网站。
  • 相关阅读:
    前端基础建设与架构05 Vite 实现:从源码分析出发,构建 bundlele 开发工程
    Spring面试题18:Spring中可以注入一个null和一个空字符串吗?Spring中如何注入一个java集合?
    x64内核实验5-API进0环
    在 Android 上恢复已删除音乐的 5 种简单方法
    ARM ACP
    C++11特性-智能指针
    Rider 设置选中单词侧边高亮,去除警告建议高亮
    unity——通过点击按钮进行场景切换
    C# —— 逻辑运算符
    软考 系统架构设计师系列知识点之特定领域软件体系结构DSSA(2)
  • 原文地址:https://www.cnblogs.com/YataoFeng/p/18206455