自定义注解实现表单重复提交
一、什么是表单重复提交
表单重复提交是指用户在短时间内多次提交相同的表单数据。
这种情况可能会导致一些问题,例如重复执行某些操作、重复发送邮件、重复购买等。
这对于网站和应用程序来说是一个常见的问题,需要采取措施来防止或处理重复提交。
二、表单重复提交场景
表单重复提交可能发生在以下情况下:
-
用户点击提交按钮多次:用户可能会因为网络延迟或操作不确定性而多次点击提交按钮。这会导致服务器接收到多个相同的表单请求。
-
网络异常导致重复提交:在网络不稳定的情况下,表单提交请求可能会在传输过程中出现问题,导致用户认为提交失败,再次提交表单。然而,之前的请求可能已经成功到达服务器,从而导致重复提交。
-
页面跳转后的重复提交:当用户提交表单后,如果页面发生重定向或跳转,用户可能会返回上一页并再次提交表单,从而导致重复提交。
三、如何防止表单重复提交
-
前端防止重复提交:可以通过禁用提交按钮、在提交后禁用按钮、添加表单提交状态标识等方式来防止用户多次点击提交按钮。
-
后端防止重复提交:在服务器端可以采取以下措施来防止表单重复提交:
-
生成唯一的表单令牌(CSRF Token)并将其嵌入表单中。服务器在处理表单提交时验证令牌的有效性,如果令牌已经使用过,则拒绝重复提交。
-
使用重定向来处理表单提交,并在处理完表单后将用户重定向到另一个页面,避免用户返回原始页面并再次提交。
在服务器端记录已经处理过的表单请求,例如可以使用缓存、数据库或会话来存储已处理的表单请求的标识,如果收到重复的请求,则拒绝处理。 -
幂等性设计:通过设计具有幂等性的操作可以避免重复提交带来的问题。幂等操作是指多次执行相同操作的结果与执行一次相同操作的结果相同。例如,向数据库插入数据时,可以使用唯一约束或主键冲突处理来确保同一数据不会被多次插入。
-
总的来说,表单重复提交是一个常见的问题,可能会导致一系列的问题和混乱。通过前端和后端的措施可以有效地防止或处理表单重复提交,提高用户体验和应用程序的可靠性。
四、解决方案
解决表单重复提交有多种形式,这里以Aop+自定义注解+Redis为例来介绍。
-
详细流程:当页面加载时前端请求后台,后台生成token缓存到redis并且反馈给前端,进入业务办理界面后,业务人员点击某个功能(保存),此时前端携带token发送请求到后端的前拦截器(实际业务方法之前的拦截器方法)=》匹配redis看是否是第一次点击:
-
情况1:有匹配。 =>说明是第一次点击,则立即清除后台redis中的token,并且允许继续执行业务方法=>执行业务方法=》请求到达后端的后置拦截器(实际业务方法之后的拦截器方法)=》将业务信息反馈给前台。此时【前端只要接到后台的反馈(成功、失败(异常))就重新请求后台生成token的方法】后端再次生成新token并缓存到redis,最后反馈给前端。
-
情况2:无匹配。=>说明前端仍然使用的旧token,且不是第一次点击(因为redis中没有)=>前拦截器立即拦截,业务终止,后端给予反馈提示信息【操作处理中,请稍后。。。】。
-
核心代码如下:
1. 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoRepeatSubmitAnnotation { }
2. 校验表单重复提交切面处理类
@Aspect
@Component
@Order(0)
public class NoRepeatSubmitAspect {
@Autowired
private RedisUtils redisUtils;
private static final Logger logger = LoggerFactory.getLogger(NoRepeatSubmitAspect.class);
/**
* 切入点
*/
@Pointcut("@annotation(com.yonyou.common.annotation.NoRepeatSubmitAnnotation)")
public void dataSubmit() {}
/**
* 前置通知,目标方法调用前被调用
* @param point
* @throws Throwable
* @return
*/
@Around("dataSubmit()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
Object obj;
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
String tokenKey = request.getHeader("noRepeatSubmit-token");
if (redisUtils.exists(tokenKey)) { //若存在,说明是首次请求,则通过继续走业务流程。
redisUtils.remove(tokenKey);//通过后,立即清理掉服务器端的令牌,防止前端短时间内重复提交。
obj = point.proceed();// 进入处理业务
}else{
logger.error("表单重复提交!loginName:{},url:{}",WebUtils.getBaseLoginUser(request).getLogin_name(),request.getRequestURL().toString());
throw new SystemException(ExceptionTypeEnum.REPEAT_SUBMIT.getCode());
}
return obj;
}
}
3. 生成Token的工具类
public ResponseInfo makeToken(){
LoginUser loginUser = WebUtils.getBaseLoginUser(HttpContextUtils.getHttpServletRequest());
String token = DataUtils.generateUUID() + DataUtils.getRandomNumber(6);
String key;
ResponseInfo responseInfo = new ResponseInfo();
MessageDigest md;
try {
md = MessageDigest.getInstance("md5");
byte md5[] = md.digest(token.getBytes());
BASE64Encoder encoder = new BASE64Encoder();
token = encoder.encode(md5);
key = "NoRepeatSubmit-" + loginUser.getLogin_name() + "-" + token;
redisUtils.set(key, token, 30L, TimeUnit.MINUTES);
} catch (NoSuchAlgorithmException e) {
logger.error("WTR-->生成token过程中失败!",e);
throw new SystemException("生成token工具类报错");
}
responseInfo.setSuccessContent(key);
return responseInfo;
}