如何在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
相关产品推荐
相关产品推荐

