0
点赞
收藏
分享

微信扫一扫

后端如何防止重复提交

JamFF 2022-04-24 阅读 79
java

原文链接:

https://www.zhihu.com/question/324268535/answer/2320741346

后台防止表单重复提交的三种方法_xin_shou123的博客-CSDN博客_防止表单重复提交

·

方案一:利用Session防止表单重复提交

1)步骤:

        1、在用户填写好用户名和密码的页面的时候,会向后台发送一次请求,这时服务器端会生成一个唯一的随机标识号,称为Token(令牌),同时在当前用户的Session域中保存这个Token。

        2、将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端。

        3.1、服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。

        3.2、如果两个Token相同,则处理该表单,处理完之后,清除当前用户的Session域中存储的标识号。

·

2)为什么要设置一个隐藏域?

        假如恶意用户开两个浏览器窗口(同一浏览器的窗口共用一个session)这样窗口1提交完,系统删掉session,窗口1停留着,他打开第二个窗口进入这个页面,系统又为他们添加了一个session,这个时候窗口1按下F5,那么直接重复提交!

        所以,我们必须得用hidden隐藏一个token,并且在后台比较它是否与session中的值一致,只有这样才能保证F5是不可能被重复提交的!

·

3)代码示例:

3.1)创建FormServlet,用于生成Token(令牌)

public class FormServlet extends HttpServlet {
	private static final long serialVersionUID = -884689940866074733L;

	public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String token =  UUID.randomUUID().toString() ;//创建令牌
		System.out.println("在FormServlet中生成的token:"+token);
		request.getSession().setAttribute("token", token);  //在服务器使用session保存token(令牌)
		request.getRequestDispatcher("/form.jsp").forward(request, response);//跳转到form.jsp页面
	}

	public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doGet(request, response);
	}
}

· 

3.2)在页面中使用隐藏域来存储Token(令牌)

<body>
      <form action="${pageContext.request.contextPath}/servlet/DoFormServlet" method="post">
         <%--使用隐藏域存储生成的token--%>
         <%--
             <input type="hidden" name="token" value="<%=session.getAttribute("token") %>">
         --%>
         <%--使用EL表达式取出存储在session中的token--%>

         <input type="hidden" name="token" value="${token}"/> 
             用户名:<input type="text" name="username"> 
         <input type="submit" value="提交">
     </form>
</body>

· 

3.3)后端判断用户是否是重复提交

public class DoFormServlet extends HttpServlet {
 
     public void doGet(HttpServletRequest request, HttpServletResponse response)
                 throws ServletException, IOException {
 
             boolean b = isRepeatSubmit(request);//判断用户是否是重复提交
             if(b==true){
                 System.out.println("请不要重复提交");
                 return;
             }
             request.getSession().removeAttribute("token");//移除session中的token
             System.out.println("处理用户提交请求!!");
     }
         
	 /**
	  * 判断客户端提交上来的令牌和服务器端生成的令牌是否一致
	  * @param request
	  * @return 
	  *         true 用户重复提交了表单 
	  *         false 用户没有重复提交表单
	  */
	 private boolean isRepeatSubmit(HttpServletRequest request) {
		 String client_token = request.getParameter("token");
		 //1、如果用户提交的表单数据中没有token,则用户是重复提交了表单
		 if(client_token==null){
			 return true;
		 }
		 //取出存储在Session中的token
		 String server_token = (String) request.getSession().getAttribute("token");
		 //2、如果当前用户的Session中不存在Token(令牌),则用户是重复提交了表单
		 if(server_token==null){
			return true;
		 }
		 //3、存储在Session中的Token(令牌)与表单提交的Token(令牌)不同,则用户是重复提交了表单
		 if(!client_token.equals(server_token)){
			 return true;
		 }
		 
		 return false;
	 }
 
     public void doPost(HttpServletRequest request, HttpServletResponse response)
             throws ServletException, IOException {
         doGet(request, response);
     }
 
}

·

方案二:判断请求url和数据是否和上一次相同

        推荐,非常简单,页面不需要任何传入,只需要在验证的controller方法上,写上自定义注解即可

写好自定义注解

/** 
 * 一个用户 相同url 同时提交 相同数据 验证 
 * @author Administrator 
 * 
 */  
@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.RUNTIME)  
public @interface SameUrlData {  
      
}  

写好拦截器

/** 
 * 一个用户 相同url 同时提交 相同数据 验证 
 * 主要通过 session中保存到的url 和 请求参数。如果和上次相同,则是重复提交表单 
 * @author Administrator 
 * 
 */  
public class SameUrlDataInterceptor  extends HandlerInterceptorAdapter{  
      
      @Override  
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
            if (handler instanceof HandlerMethod) {  
                HandlerMethod handlerMethod = (HandlerMethod) handler;  
                Method method = handlerMethod.getMethod();  
                SameUrlData annotation = method.getAnnotation(SameUrlData.class);  
                if (annotation != null) {  
                    if(repeatDataValidator(request))//如果重复相同数据  
                        return false;  
                    else   
                        return true;  
                }  
                return true;  
            } else {  
                return super.preHandle(request, response, handler);  
            }  
        }  
    /** 
     * 验证同一个url数据是否相同提交  ,相同返回true 
     * @param httpServletRequest 
     * @return 
     */  
    public boolean repeatDataValidator(HttpServletRequest httpServletRequest)  
    {  
        String params=JsonMapper.toJsonString(httpServletRequest.getParameterMap());  
        String url=httpServletRequest.getRequestURI();  
        Map<String,String> map=new HashMap<String,String>();  
        map.put(url, params);  
        String nowUrlParams=map.toString();//  
          
        Object preUrlParams=httpServletRequest.getSession().getAttribute("repeatData");  
        if(preUrlParams==null)//如果上一个数据为null,表示还没有访问页面  
        {  
            httpServletRequest.getSession().setAttribute("repeatData", nowUrlParams);  
            return false;  
        }  
        else//否则,已经访问过页面  
        {  
            if(preUrlParams.toString().equals(nowUrlParams))//如果上次url+数据和本次url+数据相同,则表示城府添加数据  
            {  
                  
                return true;  
            }  
            else//如果上次 url+数据 和本次url加数据不同,则不是重复提交  
            {  
                httpServletRequest.getSession().setAttribute("repeatData", nowUrlParams);  
                return false;  
            }  
              
        }  
    }  
  
}  
<mvc:interceptor>  
     <mvc:mapping path="/**"/>  
     <bean class="*.*.SameUrlDataInterceptor"/>  
</mvc:interceptor> 

 

·

方案三:使用本地锁,比如说AOP

        使用本地锁,本地锁有很多种,比如使用了 ConcurrentHashMap 并发容器 putIfAbsent 方法;使用guava cache的机制。使用Content-MD5 进行加密 只要参数不变,key存在就阻止提交。

        本地锁只适用于单机部署的应用。

①配置注解

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Resubmit {

    /**
     * 延时时间 在延时多久后可以再次提交
     *
     * @return Time unit is one second
     */
    int delaySeconds() default 20;

}

②实现一个锁

/**
 * 重复提交锁
 */
@Slf4j
public final class ResubmitLock {


    private static final ConcurrentHashMap<String, Object> LOCK_CACHE = new ConcurrentHashMap<>(200);

    private static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(5, new ThreadPoolExecutor.DiscardPolicy());


    private ResubmitLock() {
    }

    /**
     * 静态内部类 单例模式
     *
     * @return
     */
    private static class SingletonInstance {
        private static final ResubmitLock INSTANCE = new ResubmitLock();
    }

    public static ResubmitLock getInstance() {
        return SingletonInstance.INSTANCE;
    }

    public static String handleKey(String param) {
        return DigestUtils.md5Hex(param == null ? "" : param);
    }

    /**
     * 加锁 putIfAbsent 是原子操作保证线程安全
     *
     * @param key   对应的key
     * @param value
     * @return
     */
    public boolean lock(final String key, Object value) {
        return Objects.isNull(LOCK_CACHE.putIfAbsent(key, value));
    }

    /**
     * 延时释放锁 用以控制短时间内的重复提交
     *
     * @param lock         是否需要解锁
     * @param key          对应的key
     * @param delaySeconds 延时时间
     */
    public void unLock(final boolean lock, final String key, final int delaySeconds) {
        if (lock) {
            EXECUTOR.schedule(() -> {
                LOCK_CACHE.remove(key);
            }, delaySeconds, TimeUnit.SECONDS);
        }
    }
}

③AOP 切面

/**
 * 数据重复提交校验
 **/
@Log4j
@Aspect
@Component
public class ResubmitDataAspect {

    private final static String DATA = "data";
    private final static Object PRESENT = new Object();

    @Around("@annotation(com.cn.xxx.annotation.Resubmit)")
    public Object handleResubmit(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //获取注解信息
        Resubmit annotation = method.getAnnotation(Resubmit.class);
        int delaySeconds = annotation.delaySeconds();
        Object[] pointArgs = joinPoint.getArgs();
        String key = "";
        //获取第一个参数
        Object firstParam = pointArgs[0];
        if (firstParam instanceof RequestDTO) {
            //解析参数
            JSONObject requestDTO = JSONObject.parseObject(firstParam.toString());
            JSONObject data = JSONObject.parseObject(requestDTO.getString(DATA));
            if (data != null) {
                StringBuffer sb = new StringBuffer();
                data.forEach((k, v) -> {
                    sb.append(v);
                });
                //生成加密参数 使用了content_MD5的加密方式
                key = ResubmitLock.handleKey(sb.toString());
            }
        }
        //执行锁
        boolean lock = false;
        try {
            //设置解锁key
            lock = ResubmitLock.getInstance().lock(key, PRESENT);
            if (lock) {
                //放行
                return joinPoint.proceed();
            } else {
                //响应重复提交异常
                return new ResponseDTO<>(ResponseCode.REPEAT_SUBMIT_OPERATION_EXCEPTION);
            }
        } finally {
            //设置解锁key和解锁时间
            ResubmitLock.getInstance().unLock(lock, key, delaySeconds);
        }
    }
}

④测试注解使用:

@PostMapping("/posts/save")
@Resubmit(delaySeconds = 10)
public ResponseDTO<BaseResponseDataDTO> saveOrder(@RequestBodyRequestDTO<OrderDTO> requestDto) {
    // TODO 
}

·

方案四,使用分布式锁,比如说Redis:

        现在大多数部署方式都是集群,所以可以采用分布式锁,改造如下:

@Aspect
@Configuration
public class LockMethodInterceptor {

    @Autowired
    private RedisTemplate redisTemplate;

    private final static String DATA = "data";

    @Around("execution(public * *(..)) && @annotation(org.spring.springboot.interceptor.Resubmit)")
    public Object interceptor(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        Resubmit lock = method.getAnnotation(Resubmit.class);
        Object[] pointArgs = pjp.getArgs();

        String lockKey =  DigestUtils.md5Hex(getRequest(pointArgs));

        String value = UUID.randomUUID().toString();
        try {

            Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, value, lock.delaySeconds(), TimeUnit.SECONDS);
            if (!success) {
                throw new RuntimeException("重复提交");
            }
            try {
                return pjp.proceed();
            } catch (Throwable throwable) {
                throw new RuntimeException("系统异常");
            }
        } finally {
           redisTemplate.delete(lockKey);
        }
    }

    private String getRequest(Object... params) {
        if (params == null) {
            return "[]";
        }
        try {
            StringBuilder sb = new StringBuilder();
            sb.append("[");
            for (Object param : params) {
                if (param instanceof HttpServletRequest
                        || param instanceof HttpServletResponse
                        || param instanceof MultipartFile
                        || param instanceof BindResult
                        || param instanceof MultipartFile[]
                        || param instanceof ModelMap
                        || param instanceof Model
                        || param instanceof ExtendedServletRequestDataBinder
                        || param instanceof byte[]) {
                    continue;
                }

                sb.append(JSON.toJSON(param));

                sb.append(",");
            }
            if (sb.lastIndexOf(",") != -1) {
                sb.deleteCharAt(sb.lastIndexOf(","));
            }
            sb.append("]");
            return sb.toString();
        } catch (Exception e) {
            return "error happen while print log";
        }
    }

}

举报

相关推荐

0 条评论