框架中后端服务两种重复提交策略实现以及前端重复提交控制

一、概述

框架中后端服务两种重复提交策略实现方式

1、基于token方式: 该种方式在请求页面信息时需要在session中预置uuid,并且需要在页面中隐藏该uuid值,在在页面执行保存请求中携带uuid值,后端拦截器中判断请求中携带的uuid和session中的uuid是否相等,且有一方的uuid不存在都判断为重复提交

2、基于组装请求唯一标志方式:将请求携带的参数params、url、当前会话的sessionId组装为唯一标志,即 redisKey = params+url+sessionId, 以此标志redisKey 存储在redis中并设过期时间为8s ,如果相同的请求发生重复提交,则redisKey 一定相同,从而可判断重复提交行为

 

两种实现方式比较(框架中均采用了,在不同的场景下使用)

  • 基于token方式的有点是:判断更加精确,基于在页面中嵌入的uuid来判断, 无法被篡改;缺点是:一个页面不能多次提交(注意多次提交和重复提交的区别,多次提交是正常业务操作),除非增加更换uuid的机制

  • 组装请求唯一标志方式的优点是:页面中没有嵌入唯一性标志,所以该种方式支持多次提交,例如修改页面,可能在很短的时间内做重复修改;缺点是:受redisKey 失效时间控制,判断不是严格精确

总结:基于token方式重复提交验证适用于新增页面,且新增成功后页面发生跳转;组装请求唯一标志方式适用于修改页面且可多次重复提交修改,页面操作成功后是否发生跳转均可

 

框架中前端重复提控制方式

  • 前端主要基于控制提交按钮是否可用来阻断提交操作,在点击按钮后立即使按钮变为不可用,在异步请求回调中再使按钮变为可用

 

二、后端服务重复提交验证 –  基于token方式

   1、概要:

这是通过token方式控制表单重复提交的。利用拦截器拦截,@ControllerService注解监控返回值

   2、使用步骤:

 1)在进入页面的Controller方法上添加@PutSessionValue注解,该注解是往页面                         session中加入token值,待在页面获取

 2)在表单提交目的Controller方法上添加@CheckRepeatToken,改注解作用是判                       断该用户的token值是否存在重复提交,如果有,发返回007错误码

  3、注意事项:

 1)在需要验证重复提交的ajax设为同步提交:async:false,避免异步发起多个相同请求

 2)全局异常拦截方法中需判断是否存在5xx 异常,如果存在则把token在放回session中,

  如果后端发生异常,则把token继续维护到session,因为前端页面嵌入的uuid依然存在且没有发生改变

框架中后端服务两种重复提交策略实现以及前端重复提交控制_第1张图片

4、技术实现

 

重复提交验证拦截器声明

 

 

 

"/**"/>

"/api/**"/>

 "/auth/goLogin"/>

 " /auth/getKaptcha"/>

class="org.changneng.framework.frameworkbusiness.repeatCommit.RepeatCommitInterceptAdapterToken">

 

 

 

 

"/**"/>

"/api/**"/>

"/auth/goLogin"/>

" /auth/getKaptcha"/>

class="org.changneng.framework.frameworkbusiness.repeatCommit.PutSessionValueIntercor">

 

向session添加uuid标志注解

package org.changneng.framework.frameworkbusiness.repeatCommit;

 

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

 

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface PutSessionValue {

    String value() default "";

}

自定义token拦截方式注解

package org.changneng.framework.frameworkbusiness.repeatCommit;

 

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

 

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface CheckRepeatToken {

    String value() default "";

}

 

向session添加uuid拦截器

package org.changneng.framework.frameworkbusiness.repeatCommit;

 

import java.lang.reflect.Method;

import java.util.UUID;

import java.util.concurrent.TimeUnit;

 

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

 

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.web.method.HandlerMethod;

import org.springframework.web.servlet.HandlerInterceptor;

import org.springframework.web.servlet.ModelAndView;

 

public class PutSessionValueIntercor implements HandlerInterceptor {

 

    @Autowired

    private StringRedisTemplate StringRedisTemplate;

     

    @Override

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)

            throws Exception {

        try {

            if(handler instanceof HandlerMethod){

                HandlerMethod handlerMethod = (HandlerMethod)handler;

                Method method = handlerMethod.getMethod();

                PutSessionValue putSessionValue = method.getAnnotation(PutSessionValue.class);

                if(putSessionValue!=null){

                    String uuId = UUID.randomUUID().toString();

                    request.getSession().setAttribute("tokenReport", uuId);

                     

                    request.getSession().setMaxInactiveInterval(60*60*12);

                }

            }

        catch (Exception e) {

            e.printStackTrace();

        }

         

        return true;

    }

 

    @Override

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,

            ModelAndView modelAndView) throws Exception {

         

    }

 

    @Override

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)

            throws Exception {

         

    }

     

 

}

 

基于token方式实现重复提交严重拦截器

package org.changneng.framework.frameworkbusiness.repeatCommit;

 

import java.io.IOException;

import java.lang.reflect.Method;

 

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

 

import org.changneng.framework.frameworkcore.utils.JacksonUtils;

import org.changneng.framework.frameworkcore.utils.ResponseJson;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.http.HttpStatus;

import org.springframework.web.method.HandlerMethod;

import org.springframework.web.servlet.HandlerInterceptor;

import org.springframework.web.servlet.ModelAndView;

 

public class RepeatCommitInterceptAdapterToken implements HandlerInterceptor {

 

    @Override

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)

            throws Exception {

        if (handler instanceof HandlerMethod) {

            HandlerMethod handlerMethod = (HandlerMethod) handler;

            Method method = handlerMethod.getMethod();

            CheckRepeatToken isCheck = method.getAnnotation(CheckRepeatToken.class);

            String paramRequest = request.getParameter("token");

             

            try{

                if(isCheck != null){

                    //判断token参数是否为空

                    if(paramRequest == null || "".equals(paramRequest)){

                        System.out.println("参数为空");

                        response.setHeader("Content-Type""application/json;charset=UTF-8");

                          try {

                        response.getWriter().write(JacksonUtils.toJsonString(new ResponseJson().failure(HttpStatus.BAD_REQUEST.toString(),"007","不能重复提交","不能重复提交",null)));

                        response.getWriter().close();

                          catch (IOException e) {

                        e.printStackTrace();

                      }

                          return false;

                    }

                    String uuId = (String) request.getSession().getAttribute("tokenReport");

                    if(uuId!=null){

                        synchronized (this) {

                            String tempUuId = (String) request.getSession().getAttribute("tokenReport");

                            if(paramRequest.equals(tempUuId)&&!"".equals(tempUuId)){

                                System.out.println("第一次提交");

                                request.getSession().removeAttribute("tokenReport");

                                return true;

                            }else{

                                System.out.println("重复提交了");

                                response.setHeader("Content-Type""application/json;charset=UTF-8");

                                  try {

                                response.getWriter().write(JacksonUtils.toJsonString(new ResponseJson().failure(HttpStatus.BAD_REQUEST.toString(),"007","不能重复提交","不能重复提交",null)));

                                response.getWriter().close();

                                  catch (IOException e) {

                                e.printStackTrace();

                              

                              return false;

                            }

                        }

                         

                    }else{

                        System.out.println("重复提交了");

                        response.setHeader("Content-Type""application/json;charset=UTF-8");

                          try {

                        response.getWriter().write(JacksonUtils.toJsonString(new ResponseJson().failure(HttpStatus.BAD_REQUEST.toString(),"007","不能重复提交","",null)));

                        response.getWriter().close();

                          catch (IOException e) {

                        e.printStackTrace();

                      

                      return false;

                    }

                }

                 

                 

             

            }catch(Exception ex){

                ex.printStackTrace();

            }

            return true;

        /*else {

            return super.preHandle(request, response, handler);

        }*/

         

        return true;

    }

 

    @Override

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,

            ModelAndView modelAndView) throws Exception {

 

    }

 

    @Override

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)

            throws Exception {

 

    }

 

}

 

 

 

 

 

 

 

 

 

// 在跳转页面的controller方法中添加 @PutSessionValue  ,在新增保存的方法上添加 @CheckRepeatToken 注解即可

 

三、后端服务重复提交验证 –  基于组装请求唯一标志方式

 

1、概要:

    通过组装请求request唯一标志的方式控制表单重复提交的,redisKey = params+url+sessionId

   2、使用步骤:

      在表单提交目的Controller方法上添加@CheckRepeatCommit,改注解作用是判断该用户的request请求是否存在重复提交,如果有,发返回007错误码

   3、技术实现

 

自定义验证重复提交标志注解

package org.changneng.framework.frameworkbusiness.repeatCommit;

 

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

 

 

/**

 * 是否验证重复提交的注解标志

 * 备注:需要验证重复提交的controller上加此备注

 * @author changneng

 *

 */

 

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface CheckRepeatCommit {

     

}

 

重复提交验证拦截器声明

 

 

"/**"/>

"/api/**"/>

 "/auth/goLogin"/>

 " /auth/getKaptcha"/>

class="org.changneng.framework.frameworkbusiness.repeatCommit.RepeatCommitInterceptorAdapter"/>

 

基于request唯一标志重复提交验证拦截器实现

package org.changneng.framework.frameworkbusiness.repeatCommit;

 

import java.io.IOException;

import java.lang.reflect.Method;

import java.util.Date;

import java.util.HashMap;

import java.util.Map;

import java.util.concurrent.TimeUnit;

 

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

 

import org.changneng.framework.frameworkcore.utils.JacksonUtils;

import org.changneng.framework.frameworkcore.utils.ResponseJson;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.http.HttpStatus;

import org.springframework.security.core.userdetails.UsernameNotFoundException;

import org.springframework.web.method.HandlerMethod;

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

 

 

/**

 * 验证重复提交拦截器

 * 备注:放在所有拦截器的后面

 * @author changneng

 *

 */

public class RepeatCommitInterceptorAdapter extends HandlerInterceptorAdapter {

    int i=0;

 

    @Autowired

    private  StringRedisTemplate stringRedisTemplate;

    @Override

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)

            throws Exception {

        if (handler instanceof HandlerMethod) {

            HandlerMethod handlerMethod = (HandlerMethod) handler;

            Method method = handlerMethod.getMethod();

            CheckRepeatCommit isCheck = method.getAnnotation(CheckRepeatCommit.class);

            try{

            if (isCheck != null) {

                // 开始验证是否重复提交

                String params = JacksonUtils.toJsonString(request.getParameterMap());

                String url = request.getRequestURI();

                String sessionId = request.getSession().getId();

                String redisKey = params+url+sessionId;

                String tempKey = null;

                synchronized (this) {

                    tempKey = stringRedisTemplate.opsForValue().get(redisKey);

                    if(tempKey==null){

                        System.out.println("第一次进入");

                        stringRedisTemplate.opsForValue().set(redisKey, "0");

                        stringRedisTemplate.expire(redisKey, 8000, TimeUnit.MILLISECONDS);

                        //stringRedisTemplate.boundValueOps(redisKey).increment(1);

                        return true;

                    }else{

                        System.out.println("重复提交了。。。。。。");

                        response.setHeader("Content-Type""application/json;charset=UTF-8");

                          try {

                        response.getWriter().write(JacksonUtils.toJsonString(new ResponseJson().failure(HttpStatus.BAD_REQUEST.toString(),"007","不能重复提交","不能重复提交",null)));

                        response.getWriter().close();

                          catch (IOException e) {

                        e.printStackTrace();

                      

                      return false;

                    }

                }

                 

            }

            }catch(Exception ex){

                ex.printStackTrace();

            }

            return true;

        else {

            return super.preHandle(request, response, handler);

        }

         

 

    }

     

     

}

 

四、前端重复提交控制

 

前端防止重复提交按钮控制

1、form表单中的保存按钮

 

 

<button type="button" id="submitLocalExamineForm" class="btn btn-info" >保存button>

 

 

2、保存按钮点击监听事件中控制是否可用

 

 

$("#submitLocalExamineForm").click(function() {

      document.getElementById('submitLocalExamineForm').innerHTML = "加载.."

      document.getElementById('submitLocalExamineForm').disabled = "disabled"

 

 

      var options = {

                            url : WEBPATH + '/localExamine/saveLocalExamine',

                            type : 'post',

                            dataType:"json",

                            async: false,

                            success : function(data) {

                                business.closewait();

                                debugger;

                                if (data.meta.result == "success") {

                                    swal({

                                        title : "提示",

                                        text : data.meta.message,

                                        type : "success",

                                    }, function(isConfirm) {

                                        business.addMainContentParserHtml(WEBPATH + '/localExamine/xcjc', $("#taskObjectForm").serialize()+ "&selectType=1");

                                    });

                                    return false;

                                }else if(data.meta.code == '007'){

                                    swal({ title : data.meta.message, text : "", type : "info",allowOutsideClick :true });

                                } else {

                                    swal({title:"提示", text:data.meta.message, type:"error",allowOutsideClick :true});

                                }

                                 //按钮变为可用状态

                                document.getElementById('submitLocalExamineForm').innerHTML = '保存';

                                document.getElementById('submitLocalExamineForm').removeAttribute("disabled");

                            },

                            error : function() {

                                business.closewait();

                                swal({title:"提示 ", text:"保存信息失败!", type:"error",allowOutsideClick :true});

 

 

                                //按钮变为可用状态

                                document.getElementById('submitLocalExamineForm').innerHTML = '保存';

                                document.getElementById('submitLocalExamineForm').removeAttribute("disabled");

                            }

                        }

 

 

}

 

你可能感兴趣的:(技术架构,后端)