项目实战:防刷

2023-08-31 11:21:18 浏览数 (2)

你好,我是田哥

最近在搞充电桩项目,用户使用短信验证码形式进行注册和登录,那么问题来了?

如果用户无聊或恶意的一直点发送验证码,虽然每条短信没便宜,但短信量大了,这也是一笔不小的开销的。

因此,我们需要对此做一些限制的策略。

如何限制?这里就不得不提一些限流算法。

限流的常见算法有以下三种:

  • 时间窗口算法
  • 漏桶算法
  • 令牌算法

下面来看看充电桩项目中是如何实现的。

代码语言:javascript复制
/**
* @author tianwc  公众号:java后端技术全栈、面试专栏
* @version 1.0.0
* @date 2023年05月27日 09:13
* 博客地址:<a href="http://woaijava.cc/">博客地址</a>
*/
@Component
public class LimiterUtil {
@Resource
private RedisTemplate<String, String> redisTemplate;

/**
 * 固定窗口限流算法
 *
 * @return true 限流  false 放行
 */
public boolean fixedWindow(String key, int count) {
    long countCache = redisTemplate.opsForValue().increment(key);
    return countCache > count;
}

/**
 * intervalTime 时间内 最多只能访问5次
 *
 * @param key          缓存key
 * @param currentTime  当前时间  new Date().getTime();
 * @param intervalTime 有效期
 * @param count        限流次数
 * @return true 限流 false 放行
 */
public boolean slidingWindow(String key, Long currentTime, Long intervalTime, int count) {
    //发送次数 1
    redisTemplate.opsForZSet().add(key, UUID.randomUUID().toString(), currentTime);
    // intervalTime是限流的时间
    int countCache = Objects.requireNonNull(redisTemplate.opsForZSet().rangeByScore(key, currentTime - intervalTime, currentTime)).size();
    return countCache > count; 
}
}

发送短信单位时间内次数限制采用的是slidingWindow()方法的参数。

调用案例:

service层代码:

代码语言:javascript复制
public CommonResult<Boolean> sendCode(String phone, String codeCachePre, Integer msgType) {
        if (StringUtil.isEmpty(phone)) {
            return CommonResult.failed(ResultCode.VALIDATE_FAILED);
        }
        ParamValidate.isNull(phone, "phone参数为空");
        //同一个手机号码 发送次数限流
        //同一个手机号每秒 最多只能发5次
        boolean limit = limiterUtil.slidingWindow(RedisConstantPre.MESSAGE_LIMIT_KEY_PRE   phone, (new Date()).getTime(), 60000L, 5);
        if (limit) {
            return CommonResult.failed(ResultCode.SEND_MESSAGE_LIMIT);
        }
        CommonResult<MessageTemplateDto> commonResult = messageTemplateFeignClient.queryByMessageType(msgType);
        if (commonResult.getCode() != ResultCode.SUCCESS.getCode()) {
            log.error("短信模板不存在,tye={}", msgType);
            return CommonResult.failed(ResultCode.VALIDATE_FAILED);
        }
        MessageTemplateDto messageTemplateDto = commonResult.getData();
        //生成随机验证码
        String code = RandomUtil.randomNumbers(6);
        log.info("登录发送验证码:{}", code);
        //发送验证码
        String content = messageTemplateDto.getContent();
        //验证码占位符
        String newContent = content.replace(messageTemplateDto.getContentParam(), code);
        //调用MQ  异步发送短信验证码
        phoneMessageProducer.sendPhoneMessage(phone, newContent);
        //存到redis中 设置有效期 60秒
        //60秒后需要重现发送
        redisConfig.set(codeCachePre   phone, code, 60);
        return CommonResult.success(Boolean.TRUE);
    }

画个图 更好地理解:

controller层代码:

代码语言:javascript复制
/**
 * 发送登录手机验证码
 */
@PostMapping("/sendCode")
public CommonResult<Boolean> sendCode4Login(@RequestBody SendCodeReqDto sendCodeReqDto) {
  return sendCodeService.sendCode(sendCodeReqDto.getPhone(), RedisConstantPre.SEND_CODE_LOGIN_PRE, MessageTemplateTypeEnums.PAY_SUCCESS_TEMPLATE.getType());
}

测试

POST http://localhost:9006/user/sendCode Content-Type: application/json; charset=UTF-8

代码语言:javascript复制
{
  "code": 400015,
  "message": "短信发送太频繁,请隔一会再发送!",
  "data": null
}

0 人点赞