目录一、手机发送短信验证码架构图二、为什么要有图形验证码三、Kaptcha验证码注意事项四、短信验证码60s防刷方案设计4.1 验证码存储格式设计4.2 防刷机制详解4.3 多层防刷策略五、发送短信验证码流程图六、图形验证码编码最佳实践七、短信验证码发送接口开发与防刷机制代码实现一、手机发送短信验证码架构图注意事项成本控制防刷机制:限制同一手机号60秒内只能发送一次每日上限:限制同一手机号每天5次,防止恶意攻击余额预警:设置余额预警,余额低于100元时发送通知用户体验发送速度:短信一般3-5秒到达,如果超过30秒未收到,建议提示用户重发失败重试:短信发送失败时,可以重试1-2次,但不要过多重试错误提示:发送失败时给出清晰的错误提示,如短信服务繁忙,请稍后再试二、为什么要有图形验证码很多人会白嫖你的短信次数一天会白消耗你非常多的短信次数所以增加图形验证码可以有效的防止这种情况发生。三、Kaptcha验证码注意事项前端调用示例验证码URL添加时间戳参数t,防止浏览器缓存点击图片刷新验证码,提升用户体验发送短信前必须先输入图形验证码安全性服务端校验:图形验证码必须在服务端生成和校验,不能在前端校验一次性使用:验证码校验后立即删除,防止重复使用有效期限制:5分钟有效期,过期自动失效不区分大小写:使用equalsIgnoreCase比较,提升用户体验用户体验字符简单:只使用数字0-9,不使用容易混淆的字母(如O和0,I和l)无干扰线:去掉干扰线,降低识别难度点击刷新:支持点击图片刷新验证码错误提示:验证码错误时给出清晰提示性能优化缓存Producer:Kaptcha Producer对象创建成本高,应该缓存复用异步生成:如果并发高,可以考虑异步生成验证码Redis过期:设置合理的过期时间,避免Redis内存占用过高四、短信验证码60s防刷方案设计4.1 验证码存储格式设计Redis Value格式:验证码_时间戳示例:123456_1705838400000验证码部分:用于登录时校验时间戳部分:用于防刷检查为什么要存储时间戳防刷控制:计算距离上次发送的时间间隔过期管理:虽然Redis有TTL,但时间戳可以做更精细的控制日志追踪:方便排查问题时知道验证码何时发送4.2 防刷机制详解// 从Redis获取缓存值:验证码_时间戳 String cacheValue 123456_1705838400000; String[] parts cacheValue.split(_); String code parts[0]; // 验证码:123456 long sendTime Long.parseLong(parts[1]); // 发送时间戳 // 计算时间间隔 long now System.currentTimeMillis(); long interval now - sendTime; // 距离上次发送的毫秒数 // 如果小于60秒,拒绝发送 if (interval 60 * 1000) { return JsonData.buildResult(BizCodeEnum.CODE_LIMITED); }4.3 多层防刷策略前端防刷:发送后按钮禁用60秒,防止误点击后端防刷:检查Redis中上次发送时间,硬性限制60秒图形验证码:发送前必须先通过图形验证码,防止机器人每日上限:(可选)限制同一手机号每天最多发送5次五、发送短信验证码流程图六、图形验证码编码最佳实践验证码工具类Slf4j Component public class CaptchaUtil { private final Producer captchaProducer; private final StringRedisTemplate redisTemplate; private static final String CAPTCHA_KEY_PREFIX captcha:; private static final int CAPTCHA_EXPIRE_MINUTES 5; public CaptchaUtil(StringRedisTemplate redisTemplate) { this.redisTemplate redisTemplate; this.captchaProducer createCaptchaProducer(); } /** * 生成图形验证码并返回图片流 * param identifier 手机号或邮箱 * param type 验证码类型login/register * return 验证码图片的字节数组 */ public byte[] generateCaptchaImage(String identifier, String type) { // 生成验证码文本 String captchaText captchaProducer.createText(); // 将验证码保存到Redis String captchaKey getCaptchaKey(identifier, CheckCodeTypeEnum.valueOf(type).name()); log.info(保存图形验证码的key{}, captchaKey); redisTemplate.opsForValue().set(captchaKey, captchaText, CAPTCHA_EXPIRE_MINUTES, TimeUnit.MINUTES); // 生成验证码图片 BufferedImage image captchaProducer.createImage(captchaText); // 转换为字节数组 try { ByteArrayOutputStream outputStream new ByteArrayOutputStream(); ImageIO.write(image, jpg, outputStream); return outputStream.toByteArray(); } catch (IOException e) { throw new RuntimeException(生成验证码图片失败, e); } } /** * 校验验证码 * param identifier 手机号或邮箱 * param type 验证码类型login/register */ public void verifyCaptcha(String identifier, String captcha, String type) { String captchaKey getCaptchaKey(identifier, type); log.info(验证图形验证码的key{}, captchaKey); String savedCaptcha redisTemplate.opsForValue().get(captchaKey); if (savedCaptcha null) { throw new RuntimeException(验证码已过期); } if (!savedCaptcha.equalsIgnoreCase(captcha)) { throw new RuntimeException(验证码错误); } // 验证通过后删除验证码 redisTemplate.delete(captchaKey); } private String getCaptchaKey(String identifier, String type) { return CAPTCHA_KEY_PREFIX type : identifier; } private Producer createCaptchaProducer() { Properties properties new Properties(); // 图片宽度 properties.setProperty(kaptcha.image.width, 120); // 图片高度 properties.setProperty(kaptcha.image.height, 40); // 字体大小 properties.setProperty(kaptcha.textproducer.font.size, 30); // 字体颜色 properties.setProperty(kaptcha.textproducer.font.color, black); // 字符长度 properties.setProperty(kaptcha.textproducer.char.length, 4); // 字体 properties.setProperty(kaptcha.textproducer.font.names, Arial); // 只使用数字 properties.setProperty(kaptcha.textproducer.char.string, 0123456789); // 字符间距 properties.setProperty(kaptcha.textproducer.char.space, 4); // 背景颜色 properties.setProperty(kaptcha.background.clear.from, white); properties.setProperty(kaptcha.background.clear.to, white); // 干扰线颜色 properties.setProperty(kaptcha.noise.color, gray); // 干扰线数量 properties.setProperty(kaptcha.noise.impl, com.google.code.kaptcha.impl.NoNoise); Config config new Config(properties); DefaultKaptcha defaultKaptcha new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; } }RestController RequiredArgsConstructor RequestMapping(/api/v1/account) Tag(name 账号服务, description 账号服务接口) public class AccountController { private final CaptchaUtil captchaUtil; private final AccountService accountService; /** * 获取图形验证码 * * param identifier 手机号或邮箱 * param type 验证码类型login/register * return 验证码图片的字节数组 */ RequestMapping(value /captcha, produces MediaType.IMAGE_JPEG_VALUE) public byte[] captcha(RequestParam(value identifier, required true) String identifier, RequestParam(value type, required true) String type) { return captchaUtil.generateCaptchaImage(identifier, type); }七、短信验证码发送接口开发与防刷机制代码实现请求实体类/** * 发送验证码请求参数 */ Data Schema(description 发送验证码请求参数) public class SendCheckCodeReq { Schema(description 手机号或邮箱, example 13800138000) private String identifier; Schema(description 图形验证码, example 8264) private String captcha; Schema(description 验证码类型:login-登录,register-注册, example login) private String type; }Override public JsonData sendCheckCode(SendCheckCodeReq req) { log.info(发送短信验证码,手机号:{},类型:{}, req.getIdentifier(), req.getType()); // 1. 校验图形验证码 try { captchaUtil.verifyCaptcha(req.getIdentifier(), req.getCaptcha(), req.getType()); } catch (Exception e) { log.error(图形验证码校验失败:{}, e.getMessage()); return JsonData.buildError(e.getMessage()); } // 2. 构建Redis Key String cacheKey String.format(RedisKeyManager.CHECK_CODE_KEY, req.getType(), req.getIdentifier()); String cacheValue redisTemplate.opsForValue().get(cacheKey); // 3. 检查60秒内是否重复发送 if (cacheValue ! null !cacheValue.isEmpty()) { String[] parts cacheValue.split(_); long sendTime Long.parseLong(parts[1]); long now System.currentTimeMillis(); // 如果距离上次发送不足60秒,拒绝发送 if (now - sendTime 60 * 1000) { log.info(重复发送验证码,时间间隔:{} 秒, (now - sendTime) / 1000); return JsonData.buildResult(BizCodeEnum.CODE_LIMITED); } } // 4. 生成6位数字验证码 String smsCode CommonUtil.generateNumberCode(); String value smsCode _ System.currentTimeMillis(); // 5. 存储到Redis(10分钟有效) redisTemplate.opsForValue().set(cacheKey, value, CODE_EXPIRED, TimeUnit.MILLISECONDS); // 6. 判断是邮箱还是手机号 if (CommonUtil.isEmail(req.getIdentifier())) { // TODO: 邮箱验证码发送逻辑 log.info(邮箱验证码发送功能待实现); return JsonData.buildError(邮箱验证码功能暂未开放); } else if (CommonUtil.isPhone(req.getIdentifier())) { // 7. 发送短信验证码 try { log.info(短信验证码已生成,手机号:{},验证码:{}, req.getIdentifier(), smsCode); /* SmsDTO request SmsDTO.builder() .mobile(req.getIdentifier()) .param(smsCode) .build(); String response smsClient.sendSms(request); log.info(发送短信,结果{},response);*/ return JsonData.buildSuccess(验证码已发送请注意查收); } catch (Exception e) { log.error(发送短信失败, e); throw new RuntimeException(发送短信失败: e.getMessage()); } } return JsonData.buildResult(BizCodeEnum.CODE_TO_ERROR); }