You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在ASP.NET Core 6/7 MVC Identity中用OTP替代链接做验证

实现基于OTP的邮箱验证与密码重置流程

下面是在已集成ASP.NET Core Identity和MailKit的项目中,替换链接验证为OTP验证的具体步骤:

1. 实现OTP生成与管理服务

先创建OTP的生成、存储和验证逻辑,推荐用分布式缓存存储临时OTP(自动过期,无需手动清理)。

定义OTP服务接口与实现

public interface IOtpService
{
    // 生成OTP并存储到缓存
    string GenerateAndStoreOtp(string userId, int expirationMinutes = 10);
    // 验证OTP有效性,验证成功后删除缓存
    bool ValidateOtp(string userId, string otp);
}

public class OtpService : IOtpService
{
    private readonly IDistributedCache _cache;

    public OtpService(IDistributedCache cache)
    {
        _cache = cache;
    }

    public string GenerateAndStoreOtp(string userId, int expirationMinutes = 10)
    {
        // 生成6位安全随机OTP(用RNGCryptoServiceProvider替代Random提升安全性)
        byte[] randomBytes = new byte[3];
        using var rng = new RNGCryptoServiceProvider();
        rng.GetBytes(randomBytes);
        int otpNum = BitConverter.ToInt32(randomBytes, 0) % 1000000;
        string otp = otpNum.ToString("D6"); // 补零确保6位

        // 缓存键用用户ID+前缀,避免冲突
        string cacheKey = $"OTP_{userId}";
        _cache.SetString(cacheKey, otp, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(expirationMinutes)
        });

        return otp;
    }

    public bool ValidateOtp(string userId, string otp)
    {
        string cacheKey = $"OTP_{userId}";
        string storedOtp = _cache.GetString(cacheKey);

        if (storedOtp == null || storedOtp != otp)
        {
            return false;
        }

        // 验证成功后立即删除OTP,防止重复使用
        _cache.Remove(cacheKey);
        return true;
    }
}

注册服务

在Program.cs中添加服务注册:

builder.Services.AddScoped<IOtpService, OtpService>();
// 开发环境用内存缓存,生产环境替换为Redis等分布式缓存
builder.Services.AddDistributedMemoryCache();

2. 改造邮箱验证流程

替换原有的链接验证逻辑,改为生成OTP并发邮件,再处理用户提交的OTP。

请求邮箱验证接口

[ApiController]
[Route("account")]
public class AccountController : ControllerBase
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly IOtpService _otpService;
    private readonly IMailService _mailService; // 你的MailKit邮件服务

    public AccountController(UserManager<IdentityUser> userManager, IOtpService otpService, IMailService mailService)
    {
        _userManager = userManager;
        _otpService = otpService;
        _mailService = mailService;
    }

    [HttpPost("request-email-verification")]
    public async Task<IActionResult> RequestEmailVerification(string email)
    {
        var user = await _userManager.FindByEmailAsync(email);
        // 无论用户是否存在,都返回统一提示,避免泄露用户信息
        if (user == null) return Ok(new { Message = "验证邮件已发送,请查收" });

        string otp = _otpService.GenerateAndStoreOtp(user.Id);
        // 构造邮件内容(支持HTML格式)
        string htmlContent = $@"
<p>您好,</p>
<p>您的邮箱验证验证码是:<strong>{otp}</strong></p>
<p>验证码10分钟内有效,请尽快完成验证操作。</p>
<p>若不是您本人操作,请忽略此邮件。</p>";

        await _mailService.SendEmailAsync(email, "邮箱验证", htmlContent, isHtml: true);
        return Ok(new { Message = "验证邮件已发送,请查收" });
    }

验证OTP完成邮箱确认

[HttpPost("verify-email")]
    public async Task<IActionResult> VerifyEmail(string userId, string otp)
    {
        var user = await _userManager.FindByIdAsync(userId);
        if (user == null) return BadRequest(new { Message = "用户不存在" });

        if (!_otpService.ValidateOtp(userId, otp))
        {
            return BadRequest(new { Message = "验证码无效或已过期" });
        }

        // 标记邮箱已验证
        user.EmailConfirmed = true;
        await _userManager.UpdateAsync(user);
        return Ok(new { Message = "邮箱验证成功" });
    }
}

3. 改造密码重置流程

逻辑和邮箱验证一致,生成OTP后发送,验证通过再允许重置密码。

请求密码重置接口

[HttpPost("request-password-reset")]
public async Task<IActionResult> RequestPasswordReset(string email)
{
    var user = await _userManager.FindByEmailAsync(email);
    if (user == null) return Ok(new { Message = "重置邮件已发送,请查收" });

    string otp = _otpService.GenerateAndStoreOtp(user.Id);
    string htmlContent = $@"
<p>您好,</p>
<p>您的密码重置验证码是:<strong>{otp}</strong></p>
<p>验证码10分钟内有效,请输入验证码设置新密码。</p>
<p>若不是您本人操作,请忽略此邮件。</p>";

    await _mailService.SendEmailAsync(email, "密码重置", htmlContent, isHtml: true);
    return Ok(new { Message = "重置邮件已发送,请查收" });
}

验证OTP并重置密码

[HttpPost("reset-password")]
public async Task<IActionResult> ResetPassword(string userId, string otp, string newPassword)
{
    var user = await _userManager.FindByIdAsync(userId);
    if (user == null) return BadRequest(new { Message = "用户不存在" });

    if (!_otpService.ValidateOtp(userId, otp))
    {
        return BadRequest(new { Message = "验证码无效或已过期" });
    }

    // 生成重置令牌并执行密码重置
    string resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);
    var result = await _userManager.ResetPasswordAsync(user, resetToken, newPassword);

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

    return Ok(new { Message = "密码重置成功" });
}

4. 完善MailKit邮件服务(可选)

确保你的邮件服务支持HTML格式发送:

public interface IMailService
{
    Task SendEmailAsync(string to, string subject, string content, bool isHtml = false);
}

public class MailKitService : IMailService
{
    private readonly IConfiguration _config;

    public MailKitService(IConfiguration config)
    {
        _config = config;
    }

    public async Task SendEmailAsync(string to, string subject, string content, bool isHtml = false)
    {
        var message = new MimeMessage();
        message.From.Add(new MailboxAddress(_config["EmailSettings:SenderName"], _config["EmailSettings:SenderEmail"]));
        message.To.Add(new MailboxAddress("", to));
        message.Subject = subject;

        message.Body = new TextPart(isHtml ? "html" : "plain")
        {
            Text = content
        };

        using var client = new SmtpClient();
        await client.ConnectAsync(
            _config["EmailSettings:SmtpServer"],
            int.Parse(_config["EmailSettings:SmtpPort"]),
            SecureSocketOptions.StartTls);
        await client.AuthenticateAsync(_config["EmailSettings:SenderEmail"], _config["EmailSettings:Password"]);
        await client.SendAsync(message);
        await client.DisconnectAsync(true);
    }
}

关键注意事项

  • 安全性:验证成功后立即删除OTP,避免重复使用;生产环境不要用内存缓存,改用Redis等分布式缓存保证多实例共享OTP。
  • 用户体验:统一错误提示,不要泄露用户是否存在的信息;明确告知OTP有效期。
  • OTP强度:用RNGCryptoServiceProvider生成随机数,比Random更安全,避免被预测。

内容的提问来源于stack exchange,提问作者Abdullah Al Mahmud Khan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 21:53:27