56.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--新增功能--实现手机邮箱找回密码

发布于:2025-09-10 ⋅ 阅读:(19) ⋅ 点赞:(0)

在实际应用中,用户经常会遇到忘记密码的情况,这可能是因为长时间未使用账号、使用了多个不同的密码而混淆,或者是设备更换后未同步密码信息等各种原因。为了确保用户能够顺利找回账号访问权限,我们需要提供安全可靠的密码找回机制。在应用程序中最常见且用户体验较好的找回方式就是通过验证手机号码或邮箱来重置密码。手机号码验证通常采用发送短信验证码的方式,用户收到验证码并正确输入后即可进入重置密码流程;而邮箱验证则通常是向用户注册邮箱发送一个带有验证链接或者验证码的邮件,用户点击链接后即可进入重置密码页面。

Tip:对于邮箱验证,我们的孢子记账使用的是验证码的邮件,它的效果其实和验证链接类似。

一、Service 实现短信/邮箱验证码重置密码

重置密码的逻辑我们要在Service中实现。它的核心逻辑是首先判断找回密码的方式(手机号码找回、邮箱找回),如果是通过手机号码找回,则校验短信验证码,验证通过后再根据手机号码查询用户,查到人员后就利用AspNetCore Identity内置的GeneratePasswordResetTokenAsyncResetPasswordAsync方法将密码重置为用户设置的新密码。邮箱找回重置密码的方法类似,这里就不在复述业务逻辑了。接口和实现代码如下:

//------IAuthorizationService 接口------
/// <summary>
/// 重置密码
/// </summary>
/// <param name="resetPasswordRequest"></param>
/// <returns></returns>
Task ResetPasswordAsync(PasswordResetRequest resetPasswordRequest);

//------AuthorizationServiceImpl 实现------
/// <summary>
/// 重置密码
/// </summary>
/// <param name="resetPasswordRequest"></param>
/// <returns></returns>
public async Task ResetPasswordAsync(PasswordResetRequest resetPasswordRequest)
{
    if ((string.IsNullOrEmpty(resetPasswordRequest.Email) ||
            string.IsNullOrEmpty(resetPasswordRequest.PhoneNumber)) &&
        string.IsNullOrEmpty(resetPasswordRequest.ResetCode))
    {
        throw new BusinessException("参数错误");
    }

    SpUser? user = null;
    if (resetPasswordRequest.ResetBy == ResetEnum.Phone)
    {
        // 验证验证码
        bool isOk = await _smsService.VerifyCodeAsync(resetPasswordRequest.PhoneNumber,
            SmSPurposeEnum.ChangePassword,
            resetPasswordRequest.ResetCode);
        if (!isOk)
        {
            throw new BusinessException("验证码错误");
        }

        user = await _userManager.Users.FirstOrDefaultAsync(u => 
            u.PhoneNumber == resetPasswordRequest.PhoneNumber);
    }
    else
    {
        // 验证验证码
        var code = await _redis.GetStringAsync(resetPasswordRequest.Email);
        if (string.IsNullOrEmpty(code))
        {
            throw new BusinessException("验证码已过期或不存在");
        }

        // 验证验证码
        if (code != resetPasswordRequest.ResetCode.Trim())
        {
            throw new BusinessException("验证码错误");
        }

        // 删除Redis中的验证码
        await _redis.RemoveAsync(resetPasswordRequest.Email);

        user = await _userManager.FindByEmailAsync(resetPasswordRequest.Email);
    }

    if (user == null)
    {
        throw new BusinessException("用户不存在");
    }

    // 使用内置方法重置密码
    var token = await _userManager.GeneratePasswordResetTokenAsync(user);
    var result = await _userManager.ResetPasswordAsync(user, token, resetPasswordRequest.NewPassword);

    if (!result.Succeeded)
    {
        throw new BusinessException(string.Join(",", result.Errors.Select(e => e.Description)));
    }
}

上面的代码实现了密码重置的核心功能。这个方法首先会对传入的请求参数进行基本验证,确保必要的信息(邮箱/手机号和验证码)不为空。如果验证失败,会抛出业务异常。

在参数验证通过后,方法会根据用户选择的重置方式(手机号或邮箱)执行不同的验证流程。如果是通过手机号重置,会调用短信服务的验证方法检查验证码是否正确,然后根据手机号查找对应的用户。如果验证码错误,会抛出相应的异常。如果是通过邮箱重置,则会从Redis缓存中获取之前存储的验证码,并与用户提供的验证码进行比对。这里的验证逻辑包括检查验证码是否存在(可能已过期)以及验证码是否匹配。验证通过后,会从Redis中删除这个验证码(防止重复使用),并根据邮箱地址查找对应的用户。

在找到用户后,代码使用了ASP.NET Core Identity框架提供的密码重置功能。首先通过GeneratePasswordResetTokenAsync生成一个重置令牌,这个令牌确保了重置操作的安全性。然后使用ResetPasswordAsync方法,将用户的密码重置为新密码。如果重置过程中出现任何错误(比如密码不符合复杂度要求等),这些错误会被收集并作为异常信息抛出。

我们看到方法ResetPasswordAsync传入的参数类型是PasswordResetRequest,密码找回方式枚举类型ResetEnum,代码很简单,代码如下:

//------PasswordResetRequest------
using System.ComponentModel.DataAnnotations;
using SP.Common.Attributes;
using SP.IdentityService.Models.Enumeration;

namespace SP.IdentityService.Models.Request;

[ObjectRules(AnyOf = new[] { "Email", "PhoneNumber" })]
public class PasswordResetRequest
{
    /// <summary>
    /// 邮箱
    /// </summary>
    [StringLength(100, ErrorMessage = "邮箱长度不能超过100个字符")]
    public string? Email { get; set; }

    /// <summary>
    /// 手机号
    /// </summary>
    [MaxLength(20, ErrorMessage = "手机号长度不能超过20")]
    public string? PhoneNumber { get; set; }

    /// <summary>
    /// 验证码
    /// </summary>
    [Required(ErrorMessage = "验证码不能为空")]
    public string ResetCode { get; set; }

    /// <summary>
    /// 新密码
    /// </summary>
    [Required(ErrorMessage = "新密码不能为空")]
    [StringLength(100, MinimumLength = 6, ErrorMessage = "密码长度必须在6-100个字符之间")]
    public string NewPassword { get; set; }

    /// <summary>
    /// 找回方式
    /// </summary>
    [Required(ErrorMessage = "找回方式不能为空")]
    public ResetEnum ResetBy { get; set; }
}

//------ResetEnum------
namespace SP.IdentityService.Models.Enumeration;

/// <summary>
/// 密码找回方式
/// </summary>
public enum ResetEnum
{
    /// <summary>
    /// 邮箱找回
    /// </summary>
    Email = 1,

    /// <summary>
    /// 手机号找回
    /// </summary>
    Phone = 2,
}

首先看PasswordResetRequest类的设计,它包含了密码重置所需的所有必要信息。通过ObjectRules特性的AnyOf属性,我们指定了EmailPhoneNumber至少需要填写一个,这符合我们支持邮箱或手机号两种方式找回密码的业务需求。类中的其他属性都使用了数据注解进行基本的验证,比如StringLength控制字符串长度,Required确保必填字段不为空等。

ResetEnum枚举中,我们定义了两种密码找回方式:Email(1)表示通过邮箱找回,Phone(2)表示通过手机号找回。这个枚举的设计简单明了,便于在代码中进行类型安全的方式选择判断。

最后,这些功能通过AuthorizationController暴露给客户端,使用者只需调用ResetPassword接口即可完成密码重置操作。这种设计既保证了安全性,又提供了良好的用户体验。

最后,我们在AuthorizationControllerResetPassword Action中直接调用IAuthorizationService接口的ResetPasswordAsync方法即可

二、总结

通过集成手机短信和邮箱验证两种方式,我们为用户提供了灵活的密码重置选项。在实现过程中,我们利用了ASP.NET Core Identity框架的内置功能来确保密码重置的安全性,同时使用Redis缓存来管理验证码,并通过数据注解和自定义特性来保证数据验证的完整性。