• 表单防重复提交


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



  • 相关阅读:
    C# 大小写转换(非金额)
    DataService系列教程 (一)
    C# 大小写转换(金额)
    sql注入杂谈(一)union select
    sql注入杂谈(二)报错注入
    python正则提取txt文本
    sql注入杂谈(三)盲注
    对指定网站渗透的一些总结
    MSF的利用
    SQLMAP怎么拿shell
  • 原文地址:https://www.cnblogs.com/tilamisu007/p/12516523.html
Copyright © 2020-2023  润新知