背景:短信驗證碼接口被不法分子用來做灰產(短信郵箱轟炸機)
如何避免??的?站成為”?雞“或者被刷?
- 增加圖形驗證碼(開發?員)
- 單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();}