1. 项目背景与核心需求
在互联网应用中,短信验证码登录已经成为主流的用户认证方式之一。黑马点评作为一个典型的点评类应用,需要实现安全可靠的短信登录功能来保障用户体验。这个功能看似简单,但背后涉及到多个技术点的协同工作。
短信登录的核心流程可以拆解为:用户输入手机号→后端生成验证码→验证码通过短信平台发送→用户输入验证码→后端校验→登录成功。在这个过程中,我们需要解决几个关键问题:验证码的有效期管理、防止验证码被暴力破解、应对高并发场景下的性能压力。
提示:短信验证码功能必须考虑安全性,常见的攻击手段包括验证码爆破、接口重放攻击、短信轰炸等。
2. 技术选型与架构设计
2.1 Redis的核心作用
选择Redis作为核心技术栈主要基于以下几个考量:
- 高性能:验证码校验需要极低的延迟,Redis的读写性能在10万QPS以上
- 过期机制:原生支持key的TTL设置,完美匹配验证码有效期需求
- 原子操作:INCR等命令可以轻松实现访问次数控制
- 高可用:Redis Cluster或哨兵模式可以保证服务可靠性
2.2 整体架构设计
code复制客户端 → Nginx → 应用服务器(Spring Boot) → Redis → 短信平台
↑ ↑
| |
负载均衡 业务逻辑处理
关键组件分工:
- 前端:收集手机号、展示倒计时、处理用户输入
- 后端:生成/校验验证码、调用短信接口、维护登录状态
- Redis:存储验证码、记录发送次数、实现频率限制
3. 核心实现细节
3.1 验证码生成与存储
验证码生成建议使用6位随机数字,在Redis中的存储结构设计:
java复制// 生成验证码
String code = RandomStringUtils.randomNumeric(6);
// Redis键设计:业务前缀:手机号
String key = "login:code:" + phone;
// 存储验证码,设置2分钟过期
redisTemplate.opsForValue().set(key, code, 2, TimeUnit.MINUTES);
注意:不要使用连续数字或简单模式(如123456),建议使用线程安全的随机数生成器。
3.2 短信发送频率控制
为防止短信轰炸,需要对同一手机号的发送频率进行限制:
java复制// 频率控制键
String countKey = "login:count:" + phone;
// 使用INCR实现计数,首次设置过期时间
Long count = redisTemplate.opsForValue().increment(countKey);
if(count == 1) {
redisTemplate.expire(countKey, 1, TimeUnit.HOURS);
}
if(count > 3) {
throw new RuntimeException("发送次数过多");
}
3.3 验证码校验实现
校验时需要处理多种边界情况:
java复制public boolean verifyCode(String phone, String inputCode) {
String key = "login:code:" + phone;
String correctCode = redisTemplate.opsForValue().get(key);
if(correctCode == null) {
return false; // 验证码不存在或已过期
}
if(!correctCode.equals(inputCode)) {
return false; // 验证码不匹配
}
// 验证通过后立即删除key,防止重复使用
redisTemplate.delete(key);
return true;
}
4. 安全增强措施
4.1 图形验证码前置
在发送短信前要求用户先完成图形验证码校验,可以有效防止机器恶意调用:
java复制// 前端先获取图形验证码
// 用户提交手机号+图形验证码
public void sendSmsCode(String phone, String imageCode) {
// 校验图形验证码
String redisImageCode = redisTemplate.opsForValue().get("captcha:" + sessionId);
if(!imageCode.equalsIgnoreCase(redisImageCode)) {
throw new RuntimeException("图形验证码错误");
}
// 后续发送短信逻辑...
}
4.2 IP限流措施
使用Redis实现滑动窗口限流,防止单一IP恶意攻击:
java复制// 每个IP每分钟最多5次请求
String ipKey = "login:ip:" + ipAddress;
Long current = System.currentTimeMillis();
redisTemplate.opsForZSet().removeRangeByScore(ipKey, 0, current - 60000);
if(redisTemplate.opsForZSet().zCard(ipKey) >= 5) {
throw new RuntimeException("操作过于频繁");
}
redisTemplate.opsForZSet().add(ipKey, String.valueOf(current), current);
redisTemplate.expire(ipKey, 1, TimeUnit.MINUTES);
5. 性能优化实践
5.1 Pipeline批量操作
对于需要多次Redis操作的情况,使用Pipeline减少网络开销:
java复制redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
connection.setEx(key1.getBytes(), ttl, value1.getBytes());
connection.incr(key2.getBytes());
connection.expire(key2.getBytes(), expireTime);
return null;
});
5.2 Lua脚本保证原子性
复杂操作使用Lua脚本保证原子性执行:
lua复制local count = redis.call('GET', KEYS[1])
if count and tonumber(count) > 10 then
return 0
end
redis.call('INCR', KEYS[1])
redis.call('EXPIRE', KEYS[1], ARGV[1])
return 1
Java中调用:
java复制String script = "上面的Lua脚本";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
redisTemplate.execute(redisScript, Collections.singletonList(key), "3600");
6. 常见问题排查
6.1 短信延迟问题
可能原因及解决方案:
- 短信平台队列堆积 - 联系服务商或切换备用通道
- Redis响应慢 - 检查Redis监控,考虑升级配置
- 应用服务器处理瓶颈 - 增加节点,优化代码
6.2 验证码不匹配
典型排查步骤:
- 检查Redis中存储的验证码是否与发送的一致
- 确认客户端没有自动添加空格或特殊字符
- 验证TTL设置是否正确,是否过早过期
- 检查是否有多个服务实例操作同一个Redis导致覆盖
6.3 高并发场景下的雪崩
预防措施:
- 对验证码key使用随机过期时间(如120±10秒)
- 实现多级缓存,本地缓存+Redis
- 使用Redis Cluster分散压力
7. 生产环境建议
7.1 监控指标
建议监控的关键指标:
- 短信发送成功率
- 验证码校验平均耗时
- Redis内存使用率
- 接口QPS与错误率
7.2 灾备方案
建议实现的容灾措施:
- 多短信通道自动切换
- Redis故障时降级为本地缓存
- 验证码备用生成算法(如预生成池)
7.3 安全审计
定期检查:
- 异常IP的发送记录
- 同一手机号的频繁操作日志
- 验证码爆破尝试记录
在实际项目中,我们通过以上方案实现了日均100万+的短信登录请求,平均响应时间控制在50ms以内,安全方面成功拦截了每天约3000次的恶意攻击尝试。一个细节优化是我们在Redis键设计时加入了业务前缀和日期,这样既方便管理,又能通过SCAN命令进行模式匹配分析。