你好,我是田哥
最近在搞充电桩项目,用户使用短信验证码形式进行注册和登录,那么问题来了?
如果用户无聊或恶意的一直点发送验证码,虽然每条短信没便宜,但短信量大了,这也是一笔不小的开销的。
因此,我们需要对此做一些限制的策略。
如何限制?这里就不得不提一些限流算法。
限流的常见算法有以下三种:
- 时间窗口算法
- 漏桶算法
- 令牌算法
下面来看看充电桩项目中是如何实现的。
代码语言: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());
}
测试
代码语言:javascript复制POST http://localhost:9006/user/sendCode Content-Type: application/json; charset=UTF-8
{
"code": 400015,
"message": "短信发送太频繁,请隔一会再发送!",
"data": null
}