发送短信验证码 的实现逻辑

思路

  1. 发送手机号
  2. 校验手机号合法与否
  3. 符合,则生成验证码
  4. 验证码保存到session
  5. 发送验证码

代码实现

// 发短信
@Override
public Result sendCode(String phone, HttpSession session) throws MessagingException {
    /**
     * 1. 一级限制检查(5分钟限制):
     * 目的:检查该手机号是否在一级限制(5分钟内禁止发送)中。
     * 实现:在 Redis 的集合(Set)中查询键为 ONE_LEVERLIMIT_KEY + phone 的集合中是否存在值 "1"。
     * 逻辑:如果存在,说明该手机号在5分钟的限制期内,返回失败信息。
     */
    Boolean oneLevelLimit = stringRedisTemplate.opsForSet().isMember(ONE_LEVERLIMIT_KEY + phone, "1");
    if (oneLevelLimit != null && oneLevelLimit) {
        // 在一级限制条件内,不能发送验证码
        return Result.fail("您需要等5分钟后再请求");
    }
    /**
     * 2. 二级限制检查(20分钟限制):
     * 目的:检查该手机号是否在二级限制(20分钟内禁止发送)中。
     * 实现:同样在 Redis 的集合中查询键为 TWO_LEVERLIMIT_KEY + phone 的集合中是否存在值 "1"。
     * 逻辑:如果存在,说明该手机号在20分钟的限制期内,返回失败信息。
     */
    // 2. 判断是否在二级限制条件内
    Boolean twoLevelLimit = stringRedisTemplate.opsForSet().isMember(TWO_LEVERLIMIT_KEY + phone, "1");
    if (twoLevelLimit != null && twoLevelLimit) {
        // 在二级限制条件内,不能发送验证码
        return Result.fail("您需要等20分钟后再请求");
    }

    /**
     * 3. 一分钟内发送次数检查:
     * 目的:确保同一手机号在1分钟内最多发送一次验证码。
     * 实现:使用 Redis 的有序集合(ZSet),统计键为 SENDCODE_SENDTIME_KEY + phone 的集合中,
     * 分数在当前时间减去1分钟到当前时间之间的元素数量。
     * 逻辑:如果数量大于等于1,说明1分钟内已发送过验证码,返回失败信息。
     */
    // 3. 检查过去1分钟内发送验证码的次数
    /*
     * 1. 计算一分钟前的时间戳:
     * System.currentTimeMillis():获取当前时间的毫秒数。
     * 60 * 1000:表示60秒(即1分钟)的毫秒数。
     * System.currentTimeMillis() - 60 * 1000:计算出1分钟前的时间戳。
     * 这行代码的作用是确定时间窗口的起点,即从当前时间回溯1分钟。
     */
    long oneMinuteAgo = System.currentTimeMillis() - 60 * 1000;
    /*
     * 2. 查询过去一分钟内的发送次数:
     * stringRedisTemplate.opsForZSet():获取与Redis的有序集合(Sorted Set)相关的操作对象。
     * count(SENDCODE_SENDTIME_KEY + phone, oneMinuteAgo,
     * System.currentTimeMillis()):
     * 统计在键为SENDCODE_SENDTIME_KEY + phone的有序集合中,分数(score)在oneMinuteAgo到当前时间之间的元素数量。
     * 这行代码的作用是统计在过去1分钟内,用户对应的有序集合中有多少条记录,即用户在这段时间内请求验证码的次数。
     */
    long count_oneminute = stringRedisTemplate.opsForZSet().count(SENDCODE_SENDTIME_KEY + phone, oneMinuteAgo,
            System.currentTimeMillis());
    if (count_oneminute >= 1) {
        // 过去1分钟内已经发送了1次,不能再发送验证码
        return Result.fail("距离上次发送时间不足1分钟,请1分钟后重试");
    }
    /**
     * 4. 五分钟内发送次数检查及限制升级:产生一级限制/二级限制
     * 目的:在5分钟内发送次数达到特定阈值时,触发更高级别的限制。
     * 实现:
     * 统计5分钟内发送的验证码次数。
     * 如果发送次数达到特定值(如第8次、第11次等),将手机号加入二级限制集合,限制20分钟。
     * 如果5分钟内发送次数达到5次,加入一级限制集合,限制5分钟。
     * 逻辑:根据发送次数,动态调整限制级别,防止恶意频繁请求。
     */
    // 4. 检查5分钟内发送验证码的次数
    long fiveMinutesAgo = System.currentTimeMillis() - 5 * 60 * 1000;
    long count_fiveminute = stringRedisTemplate.opsForZSet().count(SENDCODE_SENDTIME_KEY + phone, fiveMinutesAgo,
            System.currentTimeMillis());
    if (count_fiveminute % 3 == 2 && count_fiveminute > 5) {
        // 发送了8, 11, 14, ...次,进入二级限制
        stringRedisTemplate.opsForSet().add(TWO_LEVERLIMIT_KEY + phone, "1");
        // expire():Set time to live for given key.
        stringRedisTemplate.expire(TWO_LEVERLIMIT_KEY + phone, 20, TimeUnit.MINUTES);
        return Result.fail("接下来如需再发送,请等20分钟后再请求");
    } else if (count_fiveminute == 5) {
        // 过去5分钟内已经发送了5次,进入一级限制
        stringRedisTemplate.opsForSet().add(ONE_LEVERLIMIT_KEY + phone, "1");
        stringRedisTemplate.expire(ONE_LEVERLIMIT_KEY + phone, 5, TimeUnit.MINUTES);
        return Result.fail("5分钟内已经发送了5次,接下来如需再发送请等待5分钟后重试");
    }

    /*
     * 生成并发送验证码:
     * 生成验证码:调用 MailUtils.achieveCode() 方法生成验证码。
     * 存储验证码:将生成的验证码存入 Redis,设置有效期为 LOGIN_CODE_TTL 分钟。
     * 日志记录:记录发送的验证码信息。
     * 发送验证码:调用 MailUtils.sendtoMail(phone, code) 方法,将验证码发送到指定手机号。
     */
    // 生成验证码
    String code = MailUtils.achieveCode();

    // 将生成的验证码保持到redis
    // Set the value and expiration timeout for key.
    stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

    log.info("发送登录验证码:{}", code);
    // 发送验证码
    MailUtils.sendtoMail(phone, code);
    /*
     * 记录发送时间:
     * 记录每次发送验证码的时间,用于后续的
     */
    // 更新发送时间和次数
    // Add value to a sorted set at key, or update its score if it already exists.
    stringRedisTemplate.opsForZSet().add(SENDCODE_SENDTIME_KEY + phone, System.currentTimeMillis() + "",
            System.currentTimeMillis());

    return Result.ok();
}

代码思路

  1. 方法参数里的HttpSession session并未用到(后面迁移到Redis了)
posted @ 2025-04-11 14:02  kuki'  阅读(45)  评论(0)    收藏  举报