用户注册短信验证码防刷
1.使用场景
- 注册验证
- 信息变更: 修改密码、手机号等个人信息时,确保是用户本人操作,进行短信验证
- 找回密码
- 动态登录
2.防刷目的
- 防止被黑客利用进行短信轰炸,防止浪费短信余额
3.防刷手段
- 前端图形验证码
- 前端滑动类
- 前端点击类
- 单个手机号请求限制
- 单个ip请求限制
- 手机号码真实性限制
4.实战操作
下面介绍一下图形验证码方式
- 引入依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.16</version>
</dependency>
-
短信配置
这里使用的是阿里云的短信服务
链接:https://market.aliyun.com/products/57000002/cmapi00046920.html?spm=5176.2020520132.101.1.14247218o2uxXD#sku=yuncode4092000001
#配置短信服务 sms: #添加aliyun上的appCode app-code: xxxxxxxxxxxxxxxxx template-id: M72CB42894
// 配置类 @ConfigurationProperties("sms") @Component @Data public class SmsConfig { private String appCode; private String templateId; }
// 短信发送组件 @Component @Slf4j public class SmsComponent { /** * 发送地址 */ private static final String URL_TEMPLATE = "https://jmsms.market.alicloudapi.com/sms/send?mobile=%s&templateId=%s&value=%s"; @Autowired private RestTemplate restTemplate; @Autowired private SmsConfig smsConfig; /** * @param to 手机号 * @param templateId 短信模板id * @param value 验证码 */ public void send(String to, String templateId, String value) { String url = String.format(URL_TEMPLATE, to, templateId, value); HttpHeaders headers = new HttpHeaders(); //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105 headers.set(HttpHeaders.AUTHORIZATION, "APPCODE " + smsConfig.getAppCode()); HttpEntity<String> entity = new HttpEntity<>(headers); ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class); log.info("url={},body={}", url, response.getBody()); if (response.getStatusCode() == HttpStatus.OK) { log.info("发送短信成功,响应信息:{}", response.getBody()); } else { log.error("发送短信失败,响应信息:{}", response.getBody()); } } }
// 发送短信验证码请求对象 @Data public class SendCodeRequest { /** * 图形验证码 */ private String captcha; /** * 登录的手机号/邮箱 */ private String to; }
// 验证码类型 public enum SendCodeEnum { // 用于注册 USER_REGISTER; }
-
图形验证码接口
@Autowired private StringRedisTemplate redisTemplate; /** * 图形验证码10分钟有效 */ private static final Duration CAPTCHA_CODE_EXPIRED = Duration.ofMinutes(10); /** * 获取图形验证码 * * @return */ @GetMapping("/captcha") public void getCaptcha(HttpServletRequest request, HttpServletResponse response) { // 利用hutool工具包,生成图形验证码对象 LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100, 4, 150); try { // 验证码 String code = lineCaptcha.getCode(); // 存储到redis中 redisTemplate.opsForValue().set(getCaptchaKey(request), code, CAPTCHA_CODE_EXPIRED); log.info("图形验证码code:{}", code); lineCaptcha.write(response.getOutputStream()); } catch (IOException e) { log.error("图形验证码出错:{}", e.getMessage()); } } /** * 生成/获取图形验证码缓存key * * @param request * @return */ private String getCaptchaKey(HttpServletRequest request) { // 根据request获取到ip String ip = CommonUtil.getIpAddr(request); String userAgent = request.getHeader(HttpHeaders.USER_AGENT); String key = "zhuyz:captcha:" + CommonUtil.MD5(ip + userAgent); return key; }
-
短信验证码接口
@Autowired private StringRedisTemplate redisTemplate; /** * 发送短信验证码 * * @param sendCodeRequest * @param request * @return */ @PostMapping("/send_code") public JsonData sendCode(@RequestBody SendCodeRequest sendCodeRequest, HttpServletRequest request) { // 1.校验图形验证码 String captchaKey = getCaptchaKey(request); String captchaCacheValue = redisTemplate.opsForValue().get(captchaKey); if (ObjectUtil.isNull(captchaCacheValue) && ObjectUtil.isNull(sendCodeRequest.getCaptcha()) && !StrUtil.equalsIgnoreCase(captchaCacheValue, sendCodeRequest.getCaptcha())) { return JsonData.buildResult(BizCodeEnum.CODE_CAPTCHA_ERROR); } // 2.删除图形验证码key&发送短信 redisTemplate.delete(captchaKey); return notifyService.sendCode(SendCodeEnum.USER_REGISTER, sendCodeRequest.getTo()); }
/** * 短信验证码10分钟有效期 */ public static final Duration CODE_EXPIRED = Duration.ofMinutes(10); @Autowired private StringRedisTemplate redisTemplate; @Autowired private SmsComponent smsComponent; @Autowired private SmsConfig smsConfig; /** * 1.判断redis是否存在对应的短信验证码 * 存在并且时间差小于60s,则为重复发送 * 反之则生成短信验证码,缓存并且发送短信 * * @param sendCodeEnum * @param to * @return */ @Override public JsonData sendCode(SendCodeEnum sendCodeEnum, String to) { // 1.判断是否重复发送短信 String cacheCodeKey = String.format(RedisKey.CHECK_CODE_KEY, sendCodeEnum.name(), to); // 数据格式:smsCode_timestamp String cacheCodeValue = redisTemplate.opsForValue().get(cacheCodeKey); if (StrUtil.isNotBlank(cacheCodeValue)) { List<String> split = StrUtil.split(cacheCodeValue, "_"); // 时间差 long gap = System.currentTimeMillis() - Long.parseLong(split.get(1)); if (gap < 60) { // 重复发送 return JsonData.buildResult(BizCodeEnum.CODE_LIMITED); } } // 2.生成短信验证码,缓存并且发送短信 String smsCode = RandomUtil.randomNumbers(6); // 新的缓存的值 String value = StrUtil.join("_", smsCode, System.currentTimeMillis()); redisTemplate.opsForValue().set(cacheCodeKey, value, CODE_EXPIRED); if (Validator.isEmail(to)) { // 发送邮件 } else if (Validator.isMobile(to)) { // 发送短信 smsComponent.send(to, smsConfig.getTemplateId(), smsCode); } return JsonData.buildSuccess(); }