表单防重复提交
form表单防止重复提交
4种方案:
1、js屏蔽提交按钮(只可限制按钮重复点击) 2、利用Session防止表单重复提交(需配置session分布式存储)
3、使用AOP自定义切入实现(限制了访问频率)
4、数据库增加唯一约束(简单粗暴)
5、利用token防止表单重复提交(目前最佳)
1、js屏蔽提交按钮
实现:
<script type="text/javascript"> //默认提交状态为false var commitStatus = false; function dosubmit(){ if(commitStatus==false){ //提交表单后,讲提交状态改为true commitStatus = true; return true; }else{ return false; } } </script> <body> <form action="/path/post" onsubmit="return dosubmit()" method="post"> 用户名:<input type="text" name="username"> <input type="submit" value="提交" id="submit"> </form> </body>
2、利用Session防止表单重复提交
实现原理: 1、请求页面controller加注解@FormToken(saveToken = true) 2、页面请求时拦截器生成formToken,写入session 3、页面添加<input type="hidden" id="formToken" name="formToken" value="${formToken}"> 4、提交请求传递formToken 5、对需要防止重复提交的controller加注解@FormToken(removeToken = true) 6、提交请求时拦截器,若非重复提交,移除session中的formToken
FormToken注解:
@Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface FormToken { boolean saveToken() default false; boolean removeToken() default false; }
防重复提交拦截器实现:
public class NoRepeatCommitInterceptor extends HandlerInterceptorAdapter { public NoRepeatCommitInterceptor() { } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); FormToken annotation = method .getAnnotation(FormToken.class); if (annotation != null) { boolean needSaveSession = annotation.saveToken(); if (needSaveSession) { request.getSession() .setAttribute( "formToken", UUID.randomUUID().toString()); } boolean needRemoveSession = annotation.removeToken(); if (needRemoveSession) { if (isRepeatSubmit(request)) { throw new BizException(ResultEnum.FAIL.getCode(), "不可重复提交,请稍后重试"); } request.getSession(false).removeAttribute("formToken"); } } } return true; } private boolean isRepeatSubmit(HttpServletRequest request) { String serverToken = (String) request.getSession(false).getAttribute( "formToken"); if (serverToken == null) { return true; } String clientToken = request.getParameter("formToken"); if (clientToken == null) { return true; } if (!serverToken.equals(clientToken)) { return true; } return false; } }
3、使用AOP自定义切入实现
实现原理: 1、对需要防止重复提交的Controller添加注解@NoRepeatCommit,可设置过期时间 2、提交请求时切面判断:redis setNx key,失败则拦截 key=ip_className_methodName 3、实际是限流:30秒内只允许1次请求
NoRepeatCommit注解:
@Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface NoRepeatCommit { /** * 指定时间内不可重复提交,单位秒 * * @return */ int timeout() default 30; }
防重复提交AOP实现:
@Aspect @Component public class NoRepeatCommitAspect { private static final Logger log = LoggerFactory.getLogger(NoRepeatCommitAspect.class); @Resource private RedisClient adminRedisClient; @Pointcut("@annotation(com.xxx.util.NoRepeatCommit)") public void pointcut() { } /** * @param point */ @Around("pointcut()") public Object around(ProceedingJoinPoint point) throws Throwable { log.info("进入防止重复提交切面.........."); HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); String ip = ServletUtil.getIp(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); NoRepeatCommit noRepeatCommit = method.getAnnotation(NoRepeatCommit.class); int timeout = noRepeatCommit.timeout(); if (timeout < 0) { //过期时间30秒 timeout = 30; } final int expire = timeout; boolean acquireResult = adminRedisClient.execute(key, new JedisAction<Boolean>() { @Override public Boolean action(Jedis jedis) { try { String setNxResult = jedis.set(key, UUID.randomUUID().toString(), "NX", "EX", expire); if ("OK".equals(setNxResult)) { return true; } } catch (Exception e) { log.error("acquireResult error", e); } return false; } }); if (!acquireResult) { throw new BizException(ResultEnum.FAIL, "请勿重复提交"); } //执行方法 Object object = point.proceed(); return object; } }
4、数据库增加唯一约束
5、利用token防止表单重复提交
实现原理 1、请求页面controller加注解@CreateFormToken(timeout = 60*60,keyPrefix = "SEND_MARKET_SMS") 2、页面请求时拦截器生成formToken,写入request.setAttribute("formToken", token); 3、页面添加<input type="hidden" id="formToken" name="formToken" value="${formToken!}"> 4、提交请求传递formToken 5、对需要防止重复提交的controller加注解@ConsumeFormToken 6、提交请求时拦截器,先检查formToken是否传递,若无则提示参数错误,再校验redis是否存在key=formToken,若存在则删除,不存在则提醒重复提交
7、若防重复提交业务失败,需恢复formToken(实现ResponseBodyAdvice,拦截带ConsumeFormToken注解的请求,校验返回结果,如失败重写formToken)
FormToken注解:
@Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CreateFormToken { //token过期时间,单位秒 int timeout() default 3600; //token自定义前缀 String keyPrefix() default "FORM_TOKEN_PREFIX_"; } @Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ConsumeFormToken {
}
拦截器实现:
/**
* 防重复提交拦截器
**/
public class NoRepeatCommitInterceptor extends HandlerInterceptorAdapter {
private static final Logger log = LoggerFactory.getLogger(NoRepeatCommitInterceptor.class);
@Resource
private RedisClient adminRedisClient;
public NoRepeatCommitInterceptor() {
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//创建formToken
CreateFormToken createAnnotation = method
.getAnnotation(CreateFormToken.class);
if (createAnnotation != null) {
//使用UUID以保证唯一
String tokenKey = UUID.randomUUID().toString();
String keyPrefix = createAnnotation.keyPrefix();
if (StringUtils.isNotEmpty(keyPrefix)) {
tokenKey = keyPrefix + tokenKey;
}
int timeout = createAnnotation.timeout();
if (timeout <= 0) {
//过期时间3600秒
timeout = CommonConstant.FORM_TOKEN_DEFAULT_SECOND;
}
String token = tokenKey;
int expire = timeout;
boolean acquireResult = adminRedisClient.execute(token, new JedisAction<Boolean>() {
@Override
public Boolean action(Jedis jedis) {
try {
String setNxResult = jedis.set(token, token, "NX", "EX", expire);
if (CommonConstant.OK.equals(setNxResult)) {
request.setAttribute(CommonConstant.FORM_TOKEN_NAME, token);
return true;
}
} catch (Exception e) {
log.error("acquireResult error", e);
}
return false;
}
});
return acquireResult;
}
//消费formToken
ConsumeFormToken consumeAnnotation = method
.getAnnotation(ConsumeFormToken.class);
if (consumeAnnotation != null) {
String clientToken = request.getParameter(CommonConstant.FORM_TOKEN_NAME);
if (StringUtils.isEmpty(clientToken)) {
throw new BizException(ResultEnum.FAIL.getCode(), "请求参数不合法,formToken不存在");
}
if (adminRedisClient.del(clientToken) <= 0) {
throw new BizException(ResultEnum.FAIL.getCode(), "不可重复提交,请稍后重试");
}
}
}
return true;
}
}
恢复formToken实现:
** * 恢复formToken **/ @ControllerAdvice public class FormTokenResponseBodyAdvice implements ResponseBodyAdvice { private static final Logger log = LoggerFactory.getLogger(NoRepeatCommitInterceptor.class); @Resource private RedisClient adminRedisClient; @Override public boolean supports(MethodParameter returnType, Class converterType) { Method method = returnType.getMethod(); //消费formToken ConsumeFormToken consumeAnnotation = method .getAnnotation(ConsumeFormToken.class); if (consumeAnnotation != null) { return true; } return false; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { AjaxResult result = (AjaxResult) body; //返回值成功不处理 if (result.getCode() == ResultEnum.SUCCESS.getCode()) { return body; } //通过ServerHttpRequest的实现类ServletServerHttpRequest 获得HttpServletRequest ServletServerHttpRequest sshr = (ServletServerHttpRequest) request; HttpServletRequest servletRequest = sshr.getServletRequest(); String formToken = servletRequest.getParameter(CommonConstant.FORM_TOKEN_NAME); //无formToken不处理 if (StringUtils.isEmpty(formToken)) { return body; } //恢复formToken adminRedisClient.execute(formToken, new JedisAction<Boolean>() { @Override public Boolean action(Jedis jedis) { try { String setNxResult = jedis.set(formToken, formToken, "NX", "EX", CommonConstant.FORM_TOKEN_DEFAULT_SECOND); if (CommonConstant.OK.equals(setNxResult)) { return true; } } catch (Exception e) { log.error("recoveryResult error", e); } return false; } }); return body; } }
向上吧,少年