参考链接:
防表单重复提交的四种方法:https://www.cnblogs.com/huanghuizhou/p/9153837.html
补充几点个人想法:
1. 对于前后端传递token验证的方式,每次都需要页面加载才能在后端存放token,这样会导致用户在第一次提交表单失败后就无法提交成功,需要刷新页面。
2. 利用session去给前后端的token存放获取,这对于APP来说不协调,适合用redis。
使用哪种方法要根据自己项目去考虑,比如单纯做网页的用session也不错。
我这里后台是提供给微信端和APP端,所以使用了第四种方法:使用Redis和AOP自定义切入实现
好处是校验只在后端完成,不需要前端传递token之类的,这里直接从链接中拿出来(进行少许改动,防丢,哈哈)
注意:这个方法需要自己指定防重复提交的间隔时间,在这个时间内都不能提交第二次,过了这个时间就能提交,所以时间需要把控好,可以算优点也能是缺点
实现原理:
- 自定义防止重复提交标记(@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 hhz
* @version
* @since
*/
@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 wangdy
* @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;
}
Rest方法:
//重复提交测试
@RequestMapping(value = "testCommit",method = {RequestMethod.GET,RequestMethod.POST})
@ResponseBody
@AvoidRepeatableCommit(timeout = 3)
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);
}