Skip to content

无论是采用邮箱验证码还是短信验证码,都需要防止用户进行盗刷。手机验证码厂商会额外产生费用,而邮箱验证码虽然无需额外收费,但是被大量盗刷,带宽、连接等都被占用,仍会导致系统无法正常使用。

需求分析

验证码5分钟过期,一分钟内不准重复发送。

我们对Redis的Key和Value进行特殊处理来实现防刷设计,使用阿里云发送短信可查看使用阿里云短信服务发送验证码

  • Key采用验证码类型加上邮箱地址/手机号码组成,例如register_code_1919301983@qq.com,
    • register_code代表该验证码类型是注册验证码
    • 1919301983@qq.com是邮箱地址,如果是短信验证码则改成手机号码
    • 设置过期时间为5分钟,代表该验证码5分钟内有效
  • Value由验证码加上当前系统时间的毫秒值组成,例如0025_1660530518000
    • 系统根据key获取到value时,使用split根据"_"进行字符串截取得到一个数组,下标为0的就是验证码
    • 下标为1的就是发送验证码的时间,我们使用当前系统时间的毫秒值减去发送验证码的毫秒值,如果在1分钟内,则提醒用户N秒后重新发送验证码
    • 时间方面都可以根据自己的业务进行设计

发送验证码实战

java
public boolean sendRegisterCode(String mail) {
    // 1.校验邮箱格式
    boolean isEmail = CommonUtils.isEmail(mail);
    if (!isEmail) throw new DefaultException("请输入正确的邮箱格式");

    // 2.查看邮箱是否注册
    Boolean isMember = redisTemplate.opsForSet().isMember(CacheKey.MALL_KEY, mail);
    if (isMember) throw new DefaultException("该邮箱已注册");

    // 3.获取验证码的Key
    String cacheCodeKey = getCacheCode(CodeEnum.REGISTER, mail);

    // 4.从Redis获取验证码,如果不为空判断是否大于一分钟,如果没有则生成验证码并存储到Redis
    String cacheCodeValue = (String) redisTemplate.opsForValue().get(cacheCodeKey);
    if (StringUtils.isNotBlank(cacheCodeValue)) {
        // 4.1 获取时间戳,判断是否大于0
        long ttl = Long.parseLong(cacheCodeValue.split("_")[1]);
        if (System.currentTimeMillis() - ttl < 1000 * 60) {
            log.info("重复发送验证码,时间间隔:{} 秒", (60 - ((System.currentTimeMillis() - ttl) / 1000)));
            throw new DefaultException("请" + (60 - ((System.currentTimeMillis() - ttl) / 1000)) + "s后发送验证码");
        }
    }

    // 5.生成验证码,验证码由:验证码_毫秒值
    String code = RandomUtil.randomNumbers(6) + "_" + System.currentTimeMillis();
    // 6.存储验证码
    redisTemplate.opsForValue().set(cacheCodeKey, code, 5, TimeUnit.MINUTES);
    // 7.发送验证码
    MailUtil.send(mail, "星空航班-用户注册验证码", String.format(CacheKey.MALL_CODE_HTML, "注册", code.split("_")[0], code.split("_")[0]), true);
    return true;
}

验证验证码是否正确

java
public boolean mailRegister(RegisterParam param) {
    // 1.获取验证码的Key
    String cacheCodeKey = getCacheCode(CodeEnum.REGISTER, param.getMail());
    
    // 2.查询验证码
    String cacheCodeValue = (String) redisTemplate.opsForValue().get(cacheCodeKey);
    
    // 3.如果没有找到验证码,代表验证码以及过期了
    if (StringUtils.isBlank(cacheCodeValue)) throw new DefaultException("验证码有误,请重新输入");
    
    // 4.判断验证码是否正确
    if (!param.getCode().equals(cacheCodeValue.split("_")[0])) throw new DefaultException("验证码有误,请重新输入");

    // TODO 业务逻辑
    return true;
}