思路
- 发送手机号
- 校验手机号合法与否
- 符合,则生成验证码
- 验证码保存到session
- 发送验证码

代码实现
// 发短信
@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();
}
代码思路
- 方法参数里的HttpSession session并未用到(后面迁移到Redis了)