背景:短信验证码接口被不法分子用来做灰产(短信邮箱轰炸机)
如何避免⾃⼰的⽹站成为”⾁鸡“或者被刷?
- 增加图形验证码(开发⼈员)
- 单IP请求次数限制(开发⼈员)
防刷之图形验证码(⾕歌kaptcha)
<dependency><groupId>com.baomidou</groupId><artifactId>kaptcha-spring-boot-starter</artifactId>
</dependency>
配置
package com.huoranger.dlink.config;import com.google.code.kaptcha.Constants;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.Properties;/*** @author huoranger* @since 2025-09-05*/
@Configuration
public class CaptchaConfig {/*** 验证码配置* Kaptcha配置类名** @return*/@Bean@Qualifier("captchaProducer")public DefaultKaptcha kaptcha() {DefaultKaptcha kaptcha = new DefaultKaptcha();Properties properties = new Properties();
// properties.setProperty(Constants.KAPTCHA_BORDER, "yes");
// properties.setProperty(Constants.KAPTCHA_BORDER_COLOR, "220,220,220");
// //properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "38,29,12");
// properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "147");
// properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "34");
// properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "25");
// //properties.setProperty(Constants.KAPTCHA_SESSION_KEY, "code");//验证码个数properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Courier");//字体间隔properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_SPACE,"8");//干扰线颜色
// properties.setProperty(Constants.KAPTCHA_NOISE_COLOR, "white");//干扰实现类properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");//图片样式properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.WaterRipple");//文字来源properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789");Config config = new Config(properties);kaptcha.setConfig(config);return kaptcha;}
}
接口,使用浏览器指纹作为redis 的key,与客户端进行绑定:
/*** 生成验证码*/@GetMapping("captcha")public void getCaptcha(HttpServletRequest request, HttpServletResponse response){String captchaText = producer.createText();log.info("验证码内容:{}",captchaText);// 存储Redis,配置过期时间redisTemplate.opsForValue().set(getCaptchaKey(request), captchaText, CAPTCHA_CODE_EXPIRED, TimeUnit.MILLISECONDS);BufferedImage image = producer.createImage(captchaText);try (ServletOutputStream outputStream = response.getOutputStream();){ImageIO.write(image,"jpg", outputStream);outputStream.flush();} catch (IOException e) {log.info("获取流出错:{}",e.getMessage());}}/*** 发送短信验证码* @return*/@PostMapping("/send_code")public ResponseData sendCode(@RequestBody SendCodeRequest sendCodeRequest, HttpServletRequest request){String key = getCaptchaKey(request);String cacheCaptcha = redisTemplate.opsForValue().get(key);String captcha = sendCodeRequest.getCaptcha();if (cacheCaptcha != null && cacheCaptcha.equalsIgnoreCase(captcha)){//成功redisTemplate.delete(key);return notifyService.sendCode(SendCodeEnum.USER_REGISTER,sendCodeRequest.getTo());}else {return ResponseData.buildResult(BizCodeEnum.CODE_ERROR);}}private String getCaptchaKey(HttpServletRequest request){String ip = CommonUtil.getIpAddr(request);String userAgent = request.getHeader("User-Agent");String key = "account-service:captcha:" + CommonUtil.MD5(ip + userAgent);log.info("验证码key:{}", key);return key;}
防刷之禁止重复发送短信
- 60秒后才可以᯿新发送短信验证码
- 发送的短信验证码10分钟内有效
方案:
⽅式⼀:前端增加校验倒计时,不到60秒按钮不给点击
- 简单
- 不安全,存在绕过的情况
⽅式⼆:增加Redis存储,发送的时候设置下额外的key,并且60秒后过期
- ⾮原⼦操作,存在不⼀致性
- 增加的额外的key - value存储,浪费空间
⽅式三:基于原先的key拼装时间戳
- 好处:满⾜了当前节点内的原⼦性,也满⾜业务需求
@Overridepublic ResponseData sendCode(SendCodeEnum sendCodeEnum, String to) {String cacheKey = String.format(RedisKey.CHECK_CODE_KEY,sendCodeEnum.name(),to);String cacheValue = redisTemplate.opsForValue().get(cacheKey);//如果不为空,再判断是否是60秒内重复发送 0122_232131321314132if (StringUtils.isNotBlank(cacheValue)){long ttl = Long.parseLong(cacheValue.split("_")[1]);//当前时间戳-验证码发送的时间戳,如果小于60秒,则不给重复发送long leftTime = CommonUtil.getCurrentTimestamp() - ttl;if (leftTime < (60 * 1000)){log.info("重复发送短信验证码,时间间隔:{}秒",leftTime);return ResponseData.buildResult(BizCodeEnum.CODE_LIMITED);}}String code = CommonUtil.getRandomCode(6);//生成拼接好验证码String value = code + CommonUtil.getCurrentTimestamp();redisTemplate.opsForValue().set(cacheKey, value, CODE_EXPIRED, TimeUnit.MILLISECONDS);if (CheckUtil.isEmail(to)){//发送邮箱验证码 TODO}else if (CheckUtil.isPhone(to)){//发送手机验证码smsComponent.send(to, smsConfig.getTemplateId(),code);}return ResponseData.buildSuccess();}