using Dpz.Core.MessageQueue.Abstractions;
using Dpz.Core.Public.ViewModel.Messages;
using Microsoft.AspNetCore.RateLimiting;

namespace Dpz.Core.Web.Controllers;

/// <summary>
/// 邮箱验证码控制器
/// </summary>
public class EmailVerifyController(
    IConfiguration configuration,
    IMessagePublisher<SendEmailVerifyCodeMessage> verifyCodePublisher,
    IFusionCache fusionCache,
    ILogger<EmailVerifyController> logger
) : Controller
{
    private readonly bool _emailVerifyEnabled =
        configuration.GetSection("EmailVerifyServiceOpen").Get<bool?>() ?? true;

    /// <summary>
    /// 验证码缓存key前缀
    /// </summary>
    private const string VerifyCodeCachePrefix = "EmailVerifyCode:";

    /// <summary>
    /// 发送时间缓存key前缀
    /// </summary>
    private const string SendTimeCachePrefix = "EmailVerifySendTime:";

    /// <summary>
    /// 发送间隔限制(秒)
    /// </summary>
    private const int SendIntervalSeconds = 60;

    /// <summary>
    /// 验证码有效期
    /// </summary>
    private static readonly TimeSpan VerifyCodeExpiration = TimeSpan.FromMinutes(30);

    /// <summary>
    /// 验证通过后免验证时长
    /// </summary>
    private static readonly TimeSpan VerifyPassedExpiration = TimeSpan.FromDays(3);

    /// <summary>
    /// 检查当前用户是否需要邮箱验证
    /// </summary>
    /// <returns></returns>
    [HttpGet]
    public async Task<IActionResult> CheckNeedVerify([FromQuery] string? email = null)
    {
        // 如果用户已登录,不需要验证
        if (User.Identity?.IsAuthenticated == true)
        {
            return Json(new ResultInfo(true, new { needVerify = false }));
        }

        // 服务未开启,不需要验证
        if (!_emailVerifyEnabled)
        {
            return Json(new ResultInfo(true, new { needVerify = false }));
        }

        // 服务开启但邮箱为空时,仍需要验证
        if (string.IsNullOrWhiteSpace(email))
        {
            return Json(new ResultInfo(true, new { needVerify = true }));
        }

        var verifyPassedCacheKey = Request.BuildEmailVerifyPassedCacheKey(email);
        var verified = await fusionCache.GetOrDefaultAsync<bool>(verifyPassedCacheKey);

        // 匿名用户根据配置决定是否需要验证
        return Json(new ResultInfo(true, new { needVerify = !verified }));
    }

    /// <summary>
    /// 发送验证码
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    [HttpPost, EnableRateLimiting("comment")]
    public async Task<IActionResult> SendVerifyCode([FromBody] SendVerifyCodeRequest request)
    {
        if (!_emailVerifyEnabled)
        {
            return Json(ResultInfo.ToFail("邮箱验证服务未开启"));
        }

        if (string.IsNullOrWhiteSpace(request.Email))
        {
            return Json(ResultInfo.ToFail("邮箱不能为空"));
        }

        // 检查是否在冷却时间内
        var sendTimeCacheKey = $"{SendTimeCachePrefix}{request.Email.ToLower()}";
        var lastSendTime = await fusionCache.GetOrDefaultAsync<DateTime?>(sendTimeCacheKey);

        if (lastSendTime.HasValue)
        {
            var elapsed = (DateTime.Now - lastSendTime.Value).TotalSeconds;
            if (elapsed < SendIntervalSeconds)
            {
                var remainingSeconds = (int)(SendIntervalSeconds - elapsed);
                return Json(
                    new ResultInfo($"请等待 {remainingSeconds} 秒后再试", false)
                    {
                        Data = new { remainingSeconds },
                    }
                );
            }
        }

        // 生成6位数字验证码
        var verifyCode = Random.Shared.Next(100000, 999999).ToString();

        try
        {
            // 发布验证码发送消息
            await verifyCodePublisher.PublishAsync(
                new SendEmailVerifyCodeMessage
                {
                    Email = request.Email,
                    VerifyCode = verifyCode,
                    Purpose = "评论验证",
                }
            );

            // 记录发送时间
            await fusionCache.SetAsync(
                sendTimeCacheKey,
                DateTime.Now,
                options => options.SetDuration(TimeSpan.FromSeconds(SendIntervalSeconds))
            );

            logger.LogInformation("验证码发送成功: Email={Email}", request.Email);

            return Json(
                new ResultInfo(
                    "验证码已发送",
                    new { expiresIn = (int)VerifyCodeExpiration.TotalMinutes }
                )
            );
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "验证码发送失败: Email={Email}", request.Email);
            return Json(ResultInfo.ToFail("验证码发送失败,请稍后重试"));
        }
    }

    /// <summary>
    /// 验证验证码
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    [HttpPost]
    public async Task<IActionResult> VerifyCode([FromBody] VerifyCodeRequest request)
    {
        if (!_emailVerifyEnabled)
        {
            return Json(ResultInfo.ToFail("邮箱验证服务未开启"));
        }

        if (string.IsNullOrWhiteSpace(request.Email))
        {
            return Json(ResultInfo.ToFail("邮箱不能为空"));
        }

        if (string.IsNullOrWhiteSpace(request.Code))
        {
            return Json(ResultInfo.ToFail("验证码不能为空"));
        }

        var cacheKey = $"{VerifyCodeCachePrefix}{request.Email.ToLower()}";
        var cachedCode = await fusionCache.GetOrDefaultAsync<string>(cacheKey);

        if (string.IsNullOrEmpty(cachedCode))
        {
            return Json(ResultInfo.ToFail("验证码已过期或不存在"));
        }

        if (cachedCode != request.Code)
        {
            return Json(ResultInfo.ToFail("验证码错误"));
        }

        // 验证成功后删除验证码(一次性使用)
        await fusionCache.RemoveAsync(cacheKey);

        // 记录验证通过状态,3天内免验证
        await fusionCache.SetAsync(
            Request.BuildEmailVerifyPassedCacheKey(request.Email),
            true,
            options => options.SetDuration(VerifyPassedExpiration)
        );

        logger.LogInformation("验证码验证成功: Email={Email}", request.Email);

        return Json(ResultInfo.ToSuccess());
    }

    /// <summary>
    /// 发送验证码请求
    /// </summary>
    public class SendVerifyCodeRequest
    {
        public required string Email { get; set; }
    }

    /// <summary>
    /// 验证验证码请求
    /// </summary>
    public class VerifyCodeRequest
    {
        public required string Email { get; set; }
        public required string Code { get; set; }
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个用于处理邮箱验证码功能的 ASP.NET Core 控制器。让我详细解释一下这个代码的功能和结构:

主要功能

这个控制器实现了完整的邮箱验证码系统,包括:

  1. 检查是否需要验证
  2. 发送验证码
  3. 验证验证码

核心组件

依赖注入

public class EmailVerifyController(
    IConfiguration configuration,           // 配置服务
    IMessagePublisher<SendEmailVerifyCodeMessage> verifyCodePublisher, // 消息发布器
    IFusionCache fusionCache,              // 缓存服务
    ILogger<EmailVerifyController> logger   // 日志记录器
) : Controller

关键常量和配置

  • _emailVerifyEnabled: 从配置中读取邮箱验证功能是否启用
  • SendIntervalSeconds = 60: 发送间隔限制(60秒)
  • VerifyCodeExpiration = 30分钟: 验证码有效期
  • VerifyPassedExpiration = 3天: 验证通过后的免验证期

三个主要接口

1. CheckNeedVerify - 检查是否需要验证

[HttpGet]
public async Task<IActionResult> CheckNeedVerify([FromQuery] string? email = null)

逻辑判断顺序:

  • 已登录用户 → 不需要验证
  • 服务未开启 → 不需要验证
  • 邮箱为空 → 需要验证
  • 检查缓存中是否已验证过 → 根据结果决定

2. SendVerifyCode - 发送验证码

[HttpPost, EnableRateLimiting("comment")]
public async Task<IActionResult> SendVerifyCode([FromBody] SendVerifyCodeRequest request)

主要流程:

  1. 验证服务是否开启和邮箱是否为空
  2. 检查发送冷却时间(60秒限制)
  3. 生成6位随机数字验证码
  4. 通过消息队列异步发送邮件
  5. 记录发送时间到缓存

特点:

  • 使用了速率限制 [EnableRateLimiting("comment")]
  • 防止频繁发送(60秒冷却期)
  • 异步消息发布模式

3. VerifyCode - 验证验证码

[HttpPost]
public async Task<IActionResult> VerifyCode([FromBody] VerifyCodeRequest request)

验证流程:

  1. 基础参数验证
  2. 从缓存中获取验证码
  3. 比对验证码是否正确
  4. 验证成功后删除验证码(确保一次性使用)
  5. 设置3天免验证状态

缓存策略

使用了三种不同的缓存键:

  • EmailVerifyCode: - 存储验证码
  • EmailVerifySendTime: - 记录发送时间
  • 验证通过状态缓存(通过扩展方法生成键名)

安全特性

  1. 频率限制: 60秒发送间隔限制
  2. 一次性验证码: 验证成功后立即删除
  3. 时效性: 验证码30分钟过期
  4. 速率限制: 使用ASP.NET Core的速率限制功能
  5. 免验证期: 验证通过后3天内免验证

设计模式

  1. 消息队列模式: 使用消息发布器异步处理邮件发送
  2. 缓存模式: 使用FusionCache进行高效缓存管理
  3. 配置模式: 通过配置控制功能开关

这是一个设计良好的验证码系统,具有完善的安全机制和用户体验优化。

评论加载中...