防止表单重复提交的前后端方法总结(@AvoidRepeatableCommit注解) 置顶!
原因:
前台操作的抖动,快速操作,网络通信或者后端响应慢,都会增加后端重复处理的概率。
情形
- 由于用户误操作,多次点击表单提交按钮。
- 后台返回数据延迟而产生的按钮重复提交
- 由于网速等原因造成页面卡顿,用户重复刷新提交页面。
- 黑客或恶意用户使用postman等工具重复恶意提交表单(攻击网站)。
- 这些情况都会导致表单重复提交,造成数据重复,增加服务器负载,严重甚至会造成服务器宕机。因此有效防止表单重复提交有一定的必要性。
解决方案:
思路一(前端层面):
前端页面当用户点击按钮时,时按钮置灰,不允许继续点击按钮,从而防止在延迟时间内的重复提交。
缺点:js代码容易被绕过,如进行刷新或者F12后修改置灰按钮属性。
思路二(前端层面):
前端js文件设置一个全局变量,初始值为0,点击按钮一次则将变量+1,接收到返回值时将变量-1,在调用接口前判断变量是否为1,不为1则不调用接口,相当于一个开关,0=开 1=关。
思路三(数据库层面):
数据库添加唯一约束条件,如用户名、邮箱、电话等以确保数据库只能添加一条数据。
数据库添加唯一性约束条件sql:
alter table tableName_xxx add unique key uniq_xxx(field1, field2)
这种方法需要在代码中加入捕捉插入数据异常:
try {
// insert
} catch (DuplicateKeyException e) {
logger.error("user already exist");
}
缺点:虽然简单粗暴,能有效避免重复数据插入,但是无法阻止用户恶意重复提交表单,服务器大量增加插入sql语句的执行,增加数据库和服务器的负荷。
思路四(后台层面):
使用Redis和AOP自定义切入实现:
- 自定义防止重复提交标记(@AvoidRepeatableCommit)。
- 对需要防止重复提交的Congtroller里的mapping方法加上该注解。
- 新增Aspect切入点,为@AvoidRepeatableCommit加入切入点。
- 每次提交表单时,Aspect都会保存当前key到reids(须设置过期时间)。
- 重复提交时Aspect会判断当前redis是否有该key,若有则拦截。
自定义标签:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 避免重复提交
* @author lz
* @version
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AvoidRepeatableCommit {
/**
* 指定时间内不可重复提交,单位秒
* @return
*/
long timeout() default 5 ;
}
自定义切入点Aspect:
package com.xx.web;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import com.xx.web.util.DynamicRedisUtil;
import com.xx.web.util.IPUtil;
/**
* 重复提交aop
*
* @author
* @date
*/
@Aspect
@Component
@EnableAspectJAutoProxy(exposeProxy=true)
public class AvoidRepeatableCommitAspect {
@Autowired
HttpServletRequest request; //这里可以获取到request
/**
* @param point
*/
@Around("@annotation(com.xx.web.AvoidRepeatableCommit)")
public Object around(ProceedingJoinPoint point) throws Throwable {
String ip = IPUtil.getIpAddr(request);
//获取注解
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
//目标类、方法
String className = method.getDeclaringClass().getName();
String name = method.getName();
String ipKey = String.format("%s#%s",className,name);
int hashCode = Math.abs(ipKey.hashCode());
String key = String.format("%s_%d",ip,hashCode);
// log.info("ipKey={},hashCode={},key={}",ipKey,hashCode,key);
AvoidRepeatableCommit avoidRepeatableCommit = method.getAnnotation(AvoidRepeatableCommit.class);
int timeout = avoidRepeatableCommit.timeout();
if (timeout < 0){
timeout = 5;
}
//用多参数set方法保证对redis操作原子性
Integer isSuccess = DynamicRedisUtil.setnxAndExpire(key, UUID.randomUUID().toString(), timeout*1000, DynamicRedisUtil.AVOID_REPEATABLE_COMMIT_DB);
if (isSuccess == 0) {
resultMap.put("errCode", 10001);
resultMap.put("errMsg", "请勿重复提交");
return JSON.toJSONString(resultMap);
}
//执行方法
Object object = point.proceed();
return object;
}
}
/**
* redis工具类方法
* 比setnx多了个保存失效时间
* @author lz
* @date 2018年8月13日 下午2:38:07
* @param key
* @param value
* @param seconds 失效时间,单位秒
* @param db
* @return 当key不存在,保存成功并返回1,当key已存在不保存并返回0
*/
public static Integer setnxAndExpire(final String key, String value, long milliseconds, int db) {
JedisPool pool = getPool();
Jedis jds = null;
boolean broken = false;
int setnx = 0;
try {
jds = pool.getResource();
jds.select(db);
String result = jds.set(key, value, "NX", "PX", milliseconds);
if ("OK".equals(result)) {
setnx = 1;
}
return setnx;
} catch (Exception e) {
broken = true;
logger.error("setString:" + e.getMessage());
} finally {
if (broken) {
pool.returnBrokenResource(jds);
} else if (jds != null) {
pool.returnResource(jds);
}
}
return setnx;
}
测试(使用):
@RequestMapping(value = "testCommit",method = {RequestMethod.GET,RequestMethod.POST})
@ResponseBody
@AvoidRepeatableCommit(timeout = 5)
public String testCommit(HttpServletRequest request){
Map<String,Object> resultMap = new HashMap<String,Object>();
try{
resultMap.put("success", true);
}catch (Exception e) {
e.printStackTrace();
resultMap.put("success", false);
}
return JSON.toJSONString(resultMap);
}
思路五(后台层面):
分布式锁详情见他人博客,推荐:
原文:https://blog.csdn.net/memmsc/article/details/80837996
最后跪求各位大佬,路过点赞~谢谢大家!
参考
- https://blog.csdn.net/zl384701202/article/details/83022920
- https://blog.csdn.net/memmsc/article/details/80837996
- https://blog.csdn.net/God_00/article/details/112016837
标题:防止表单重复提交的前后端方法总结(@AvoidRepeatableCommit注解)
作者:mmzsblog
地址:https://www.mmzsblog.cn/articles/2022/06/17/1655429359940.html
如未加特殊说明,文章均为原创,转载必须注明出处。均采用CC BY-SA 4.0 协议!
本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。若本站转载文章遗漏了原文链接,请及时告知,我们将做删除处理!文章观点不代表本网站立场,如需处理请联系首页客服。• 网站转载须在文章起始位置标注作者及原文连接,否则保留追究法律责任的权利。
• 公众号转载请联系网站首页的微信号申请白名单!
