云计算百科
云计算领域专业知识百科平台

实现发送短信验证码功能

一、前言:短信验证码 ≠ 调个接口那么简单!

很多开发者认为:“发短信验证码不就是调个第三方 API 吗?”
但真实生产环境中,你必须考虑:

  • ❌ 用户疯狂点击“获取验证码”,导致短信费用飙升
  • ❌ 黑产用脚本批量注册,消耗系统资源
  • ❌ 验证码被暴力破解,账号被盗
  • ❌ 同一手机号频繁请求,影响正常用户

一个健壮的短信验证码功能,核心不在“发短信”,而在“控频 + 安全 + 校验”。

本文将带你用 Spring Boot + Redis 实现一套可直接用于生产环境的短信验证码系统,包含:
✅ 图形验证码前置校验
✅ 滑动窗口限流(IP + 手机号双维度)
✅ 验证码存储与自动过期
✅ 一次性使用 + 防重放
✅ 完整代码示例


二、整体设计流程图

用户点击【获取验证码】

[1] 前端携带图形验证码 token

[2] 后端校验图形验证码(防止机器刷)

[3] 检查 IP 今日发送次数(防代理刷)

[4] 检查该手机号 60 秒内是否已发送(防频繁点)

[5] 生成 6 位随机码,存入 Redis(5分钟有效)

[6] 调用短信平台(阿里云/腾讯云等)发送

[7] 返回成功(前端开始倒计时)

用户提交表单 + 验证码

[8] 校验验证码是否正确 & 未过期 & 未使用

[9] 验证通过,执行业务逻辑(注册/登录/找回密码)


三、技术选型

组件作用
Spring Boot 3.x Web 框架
Redis 存储验证码、限流计数器
StringRedisTemplate 操作 Redis(字符串友好)
Kaptcha / EasyCaptcha 生成图形验证码(本文用 EasyCaptcha)
第三方短信平台 阿里云短信、腾讯云短信等

四、核心实现步骤

1. 添加依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!– 图形验证码 –>
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>


2. Redis Key 命名规范(重要!)

// 验证码存储
sms:code:{phone} → value = "123456"

// 手机号发送频率控制(60秒内只能发1次)
sms:limit:phone:{phone} → value = "1" (带 TTL 60s)

// IP 发送次数限制(每天最多20次)
sms:limit:ip:{ip} → value = 计数(带 TTL 24h)


3. 图形验证码生成接口

@RestController
public class CaptchaController {

@GetMapping("/captcha")
public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 生成验证码(4位数字)
ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 48);
captcha.setLen(4);

// 将验证码存入 Session(或 Redis,此处简化用 Session)
request.getSession().setAttribute("captcha", captcha.text());

// 输出图片
response.setContentType("image/png");
captcha.out(response.getOutputStream());
}
}

✅ 前端访问 /captcha 获取图片,提交时携带用户输入的算术结果。


4. 发送短信验证码接口(核心!)

@RestController
public class SmsController {

@Autowired
private StringRedisTemplate redisTemplate;

private static final String SMS_CODE_KEY_PREFIX = "sms:code:";
private static final String SMS_PHONE_LIMIT_PREFIX = "sms:limit:phone:";
private static final String SMS_IP_LIMIT_PREFIX = "sms:limit:ip:";

@PostMapping("/send-sms-code")
public ResponseEntity<String> sendSmsCode(
@RequestParam String phone,
@RequestParam String captchaInput,
HttpServletRequest request) {

// 1. 校验手机号格式
if (!isValidPhone(phone)) {
return ResponseEntity.badRequest().body("手机号格式错误");
}

// 2. 校验图形验证码
String sessionCaptcha = (String) request.getSession().getAttribute("captcha");
if (sessionCaptcha == null || !sessionCaptcha.equals(captchaInput)) {
return ResponseEntity.badRequest().body("图形验证码错误");
}

String clientIp = getClientIP(request);

// 3. 检查 IP 日限额(例如每天最多20次)
String ipLimitKey = SMS_IP_LIMIT_PREFIX + clientIp;
Integer ipCount = getRedisCount(ipLimitKey, 24 * 3600);
if (ipCount >= 20) {
return ResponseEntity.status(429).body("操作过于频繁,请明天再试");
}

// 4. 检查该手机号是否60秒内已发送
String phoneLimitKey = SMS_PHONE_LIMIT_PREFIX + phone;
Boolean isPhoneLimited = redisTemplate.hasKey(phoneLimitKey);
if (Boolean.TRUE.equals(isPhoneLimited)) {
return ResponseEntity.status(429).body("请60秒后再试");
}

// 5. 生成6位随机验证码
String code = String.valueOf((int) ((Math.random() * 9 + 1) * 100000));

// 6. 存入 Redis(5分钟有效)
redisTemplate.opsForValue().set(SMS_CODE_KEY_PREFIX + phone, code, 5, TimeUnit.MINUTES);

// 7. 设置限流标记(60秒)
redisTemplate.opsForValue().set(phoneLimitKey, "1", 60, TimeUnit.SECONDS);

// 8. 更新 IP 计数
redisTemplate.opsForValue().increment(ipLimitKey);
redisTemplate.expire(ipLimitKey, 24, TimeUnit.HOURS);

// 9. 【模拟】调用短信平台(实际替换为阿里云/腾讯云 SDK)
System.out.println("【测试】向 " + phone + " 发送验证码: " + code);

return ResponseEntity.ok("验证码已发送,请注意查收");
}

// 辅助方法:获取 Redis 中的计数值
private Integer getRedisCount(String key, int expireSeconds) {
String val = redisTemplate.opsForValue().get(key);
if (val == null) {
redisTemplate.opsForValue().set(key, "0", expireSeconds, TimeUnit.SECONDS);
return 0;
}
return Integer.parseInt(val);
}

// 辅助方法:获取客户端真实 IP
private String getClientIP(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}

// 手机号正则校验
private boolean isValidPhone(String phone) {
return phone.matches("^1[3-9]\\\\d{9}$");
}
}


5. 验证码校验接口(用于注册/登录)

@PostMapping("/verify-code")
public ResponseEntity<String> verifyCode(@RequestParam String phone,
@RequestParam String inputCode) {
String realCode = redisTemplate.opsForValue().get(SMS_CODE_KEY_PREFIX + phone);

if (realCode == null) {
return ResponseEntity.badRequest().body("验证码已过期");
}

if (!realCode.equals(inputCode)) {
return ResponseEntity.badRequest().body("验证码错误");
}

// 验证成功!删除验证码(一次性使用)
redisTemplate.delete(SMS_CODE_KEY_PREFIX + phone);

// TODO: 执行注册/登录逻辑

return ResponseEntity.ok("验证成功");
}

✅ 关键点:验证成功后立即删除验证码,防止重复使用!


五、安全加固措施总结

风险防御方案
机器刷验证码 前置图形验证码(或行为验证)
单手机号狂点 60秒内限1次(Redis + TTL)
IP 批量注册 IP 日限额(如20次/天)
验证码爆破 5分钟过期 + 一次性使用
短信费用失控 监控告警 + 第三方平台额度限制

💡 进阶建议:

  • 敏感操作(如改密)增加二次验证
  • 使用阿里云“风险识别”服务检测黑产 IP
  • 验证码长度可动态调整(如 6~8 位)

六、常见问题与避坑指南

❌ 坑 1:验证码存 Session

问题:集群部署时 Session 不共享
解决:必须用 Redis 存储

❌ 坑 2:只限制手机号,不限 IP

后果:黑产换号狂刷
解决:IP + 手机号 双维度限流

❌ 坑 3:验证码可重复使用

风险:中间人攻击重放
解决:验证后立即删除

❌ 坑 4:未校验手机号格式

后果:Redis 被恶意 Key 占满
解决:严格正则校验


七、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

赞(0)
未经允许不得转载:网硕互联帮助中心 » 实现发送短信验证码功能
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!