表单防重复提交

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;
    }
}

 




posted @ 2020-03-18 12:01  提拉米苏007  阅读(584)  评论(0编辑  收藏  举报