叙述
平时开发的项目中可能会出现下面这些情况:
- 由于用户误操作,多次点击表单提交按钮。
- 由于网速等原因造成页面卡顿,用户重复刷新提交页面。
- 黑客或恶意用户使用postman等工具重复恶意提交表单(攻击网站)。
这些情况都会导致表单重复提交,造成数据重复,增加服务器负载,严重甚至会造成服务器宕机。因此有效防止表单重复提交有一定的必要性。
实现原理:
- 自定义防止重复提交标记(@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;
/**
* 避免重复提交
* @ClassName: AvoidRepeatableCommit
* @date: 2019/3/26 16:38
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AvoidRepeatableCommit {
/**
* 指定时间内不可重复提交,单位毫秒
*/
long timeout() default 3000 ;
}
自定义切入点Aspect:
import com.ccx.ticket.Constants;
import com.ccx.ticket.util.HttpUtil;
import com.ccx.ticket.util.StringUtils;
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.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
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.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 重复提交aop
* @data: 2019-03-26 16:39
**/
@Aspect
@Component
@Slf4j
public class AvoidRepeatableCommitAspect {
@Autowired
private RedisTemplate redisTemplate;
/**
* @param point
*/
@Around("@annotation(com.ccx.ticket.filter.AvoidRepeatableCommit)")
public Object around(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String ip = HttpUtil.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 = Constants.REDIS_ROOT+String.format("%s_%d",ip,hashCode);
log.info("ipKey={},hashCode={},key={}",ipKey,hashCode,key);
AvoidRepeatableCommit avoidRepeatableCommit = method.getAnnotation(AvoidRepeatableCommit.class);
long timeout = avoidRepeatableCommit.timeout();
if (timeout < 0){
timeout = 3*Constants.SECOND;
}
String value = (String) redisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(value)){
return "请勿重复提交";
}
redisTemplate.opsForValue().set(key, UUID.randomUUID().toString(),timeout, TimeUnit.MILLISECONDS);
//执行方法
Object object = point.proceed();
return object;
}
}
常量类:
public class Constants {
public static final String REDIS_ROOT="WX:ccx-:";
public static final long DAY = 1000L*60*60*24;
public static final long HOUR = 1000L*60*60;
public static final long MINUTE = 1000L*60;
public static final long MINUTE_10 = 1000L*60*10;
public static final long HALF_HOUR = 1000L*60*30;
public static final long SECOND = 1000L;
}
使用方法:
@AvoidRepeatableCommit
@RequestMapping(value = "/search", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.POST)
@ResponseBody
public ResponseMsg<Map> preSearch(@RequestBody(required = false) Map<String, Object> param) {
//具体处理业务
return null;
}
效果图: