场景一:控制表单重复提交
防重提交有很多方案,从前端的按钮置灰,到后端synchronize锁、Lock锁、借助Redis语法实现简单锁、Redis+Lua分布式锁、Redisson分布式锁,再到DB的悲观锁、乐观锁、借助表唯一索引等等都可以实现防重提交,以保证数据的安全性。
这篇文章我们介绍其中一种方案–借助Redis语法实现简单锁,最终实现防重提交。
背景:
我们项目中,为了控制表单重复提交问题,会在点击页面按钮(向后端发起业务请求)后就会置灰按钮,直到后端响应后解除按钮置灰。通过按钮置灰来防止重启提交问题。但Postman、Jmeter和其他服务调用(绕过前端页面)呢?所以后端接口也要根据控制表单重复提交的问题。
后端代码可以在2个位置做控制:
一是放在gateway网关做
好处是只在一个地方加上控制代码,就可以控制所有接口的重复提交问题。坏处是控制的范围太广(比如查询接口无需控制,控制了反而多余)、定义重复提交的时间段不能灵活调整。
二是放在AOP切面做
好处是只有需要的地方才会被控制(哪里需要引用一下自定义注解即可),另外也能灵活调整定义重复提交的时间段(自定义注解里定义时间字段开放给使用者填写)。坏处是每个需要控制的地方都要加注解,会有侵入性和一定的工作量。
实现代码
1、添加自定义注解
package com.xxx.annotations;
import java.lang.annotation.*;
/**
* 自定义注解防止表单重复提交
*
* @Author WANGLINGQIANG
* @Date 2023/9/6 10:11
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 过期时间,单位毫秒
*/
long expireTime() default 500L;
}
2、添加AOP切面
package com.xxx.aop;
import com.xxx.annotations.RepeatSubmit;
import com.xxx.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* 防止表单重复提交切面
*
* @Author WANGLINGQIANG
* @Date 2023/9/6 10:13
*/
@Slf4j
@Aspect
@Component
public class RepeatSubmitAspect {
private static final String KEY_PREFIX = "repeat_submit:";
@Resource
private RedisTemplate redisTemplate;
@Pointcut("@annotation(com.xxx.annotations.RepeatSubmit)")
public void repeatSubmit() {}
@Around("repeatSubmit()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//joinPoint获取方法对象
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//获取方法上的@RepeatSubmit注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
//获取HttpServletRequest对象,以获取请求uri
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String uri = request.getRequestURI();
//拼接Redis的key,这里只是简单根据uri来判断是否重复提交。可以根据自己业务调整,比如根据用户id或者请求token等
String cacheKey = KEY_PREFIX.concat(uri);
Boolean flag = null;
try {
//借助setIfAbsent(),key不存在才能设值成功
flag = redisTemplate.opsForValue().setIfAbsent(cacheKey, "", annotation.expireTime(), TimeUnit.MILLISECONDS);
} catch (Exception e) {
//如果Redis不可用,则打印日志记录,但依然对请求放行
log.error("", e);
return joinPoint.proceed();
}
//Redis可用的情况,如果flag=true说明单位时间内这是第一次请求,放行
if (flag) {
return joinPoint.proceed();
} else {
//进入else说明单位时间内进行了多次请求,则拦截请求并提示稍后重试
throw new ServiceException("系统繁忙,请稍后重试");
}
}
}
这里利用redisTemplate的setIfAbsent()实现的,如果存在就不能set成功,set的同时设置过期时间,可以是用使用默认,也可以自己根据业务调整。
另外,cacheKey的定义,也可以根据自己的需要去调整,比如根据当前登录用户的userId、当前登录的token等。
3、使用
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@RepeatSubmit
@PostMapping
public AjaxResult add(@Validated @RequestBody SysUser user) {
//....
}
场景二:控制接口调用频率
背景:
忘记密码后通过发送手机验证码找回密码的场景。因为每发一条短信都需要收费,所以要控制发短信的频率。比如,同一个手机号在3分钟内只能发送3次短信,超过3次后则提示用户“短信发送过于频繁,请10分钟后再试”。文章来源:https://www.toymoban.com/news/detail-697475.html
实现代码
@Slf4j
@RestController
@RequestMapping("/sms")
public class SmsController {
@Resource
private ISmsService smsService;
@Resource
public RedisTemplate redisTemplate;
@PostMapping("/sendValidCode")
public Result sendValidCode(@RequestBody @Valid SmsDTO smsDTO) {
//验证手机号格式
checkPhoneNumber(smsDTO.getPhoneNumber());
//...其他验证
//拼接Redis的key(key为手机号,以控制一个手机号有限时间内容发送的次数)
String cacheKey = "sms:code:resetPwd:"+smsDTO.getPhoneNumber();
//验证发送短信次数,超过则拦截(阈值是3次,超时时间是3分钟,重试时间是10分钟)
checkSendCount(cacheKey, THRESHOLD, TIMEOUT, RETRY_TIME);
return smsService.sendMsg(smsDTO);
}
/**
* 验证发送短信次数,超过则拦截
* 该方法用lua脚本替换实现更好
*/
private void checkSendCount(String cacheKey, Long threshold, Long timeout, String retryTime) {
//首先进方法就先+1
Long count = redisTemplate.opsForValue().increment(cacheKey);
//然后比较次数,是否超过阈值
if (count > threshold) {
//超过则设置过期时间为10分钟,并提示10分钟后重试
redisTemplate.expire(cacheKey, 10L, TimeUnit.MINUTES);
throw new ServiceException("短信发送过于频繁,请" + retryTime + "分钟后再试");
} else {
//没超过3次,则累加上这一次
redisTemplate.expire(cacheKey, timeout, TimeUnit.MINUTES);
}
}
}
本章完结。
文章来源地址https://www.toymoban.com/news/detail-697475.html
到了这里,关于使用Redis控制表单重复提交控制接口访问频率的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!