重复提交的定义:

  重复提交指的是同一个请求(请求地址和请求参数都相同)在很短的时间内多次提交至服务器,从而对服务器造成不必要的资源浪费,甚至在代码不健壮的情况还会导致程序出错。

重复提交的原因或触发事件:

  • 【场景一】一次请求处理过慢,用户等不及点了多次提交按钮。
  • 【场景二】提交请求之后,用户又多次点了刷新按钮或者点了回退
  • 【场景三】同时打开了多个窗口提交数据。

重复提交的解决方案:

  • 对于【场景一】

  可以通过JS在用户点击按钮之后立即disable按钮,让他不能点。

    如果是ajax提交的方式,那可以出现一个遮罩层。在服务器响应之前不让用户在做操作。

    如果使用Jquery的$.ajax方法提交,可以将async设置成false,或者在beforeSend方法中开启遮罩层。

  • 对于【场景二】和【场景三】

简单的解决方案是:将表单提交方式改成ajax提交,然后按上面的方式控制用户只能点一次提交按钮。而且ajax方式用户也不能回退和刷新。

  对于【场景三】则需要一种稍复杂的方案就是令牌(Token),下面就来详细介绍令牌方案

  • 令牌(Token)解决方案

方案步骤

1. 开发自定义注解来区分哪些请求方法是需要控制重复提交的。

 1 @Target(ElementType.METHOD)
2 @Retention(RetentionPolicy.RUNTIME)
3 public @interface Token {
4 /**
5 * <p>保存并更新token,请在需要生成token的方法上设置save=true,例如:页面跳转的方法</p>
6 * @since 2019年4月4日
7 */
8 boolean save() default false;
9 /**
10 * <p>校验token,请在提交的方法上加上本属性</p>
11 * @since 2019年4月4日
12 */
13 boolean check() default false;
14 }

  2. 使用过滤器或拦截器,配合上文的注解生成令牌和校验令牌。

本文使用得是Spring拦截器,Spring配置文件:

 <mvc:interceptors>
<!-- token过滤器-->
<mvc:interceptor>
<mvc:mapping path="/**/*.do" />
<bean class="com.kedacom.plmext.util.TokenInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>

  令牌拦截器(TokenInterceptor)代码,令牌是用uuid的形式:

 @SuppressWarnings("unchecked")
public class TokenInterceptor extends HandlerInterceptorAdapter { /**
* <a href="https://www.cnblogs.com/namelessmyth/p/10660526.html">使用Token控制重复提交方法</a><p>
* 请求预处理方法 <p>
* 对于标注@Token(save=true)的方法自动生成或刷新token,按URL来生成 <p>
* 对于标注@Token(check=true)的方法校验客户端的token是否在session中存在,如果不存在抛异常 <p>
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//只有设有注解的方法才处理
Token annotation = method.getAnnotation(Token.class);
if (annotation != null) {
//以请求url为维度
String url = getWholeUrl(request);
boolean needCheck = annotation.check();
if (needCheck) {
// 校验令牌
checkToken(request);
}
boolean needSave = annotation.save();
if (needSave) {
// 保存或刷新令牌
saveToken(request, url);
}
}
return true;
} else {
return super.preHandle(request, response, handler);
}
} public String getWholeUrl(HttpServletRequest request) {
StringBuffer sb = new StringBuffer(request.getRequestURI());
String queryString = request.getQueryString();
if (queryString != null) {
sb.append('?').append(queryString);
}
return sb.toString();
} /**
* <p>生成或刷新Token,Token使用uuid生成.</p>
* @param tokenKey 令牌key,目前是url
* @since 2019年4月6日
*/
private void saveToken(HttpServletRequest request, String tokenKey) {
HttpSession session = request.getSession(false);
Object tokenObj = session.getAttribute("tokenMap");
Map<String, String> tokenMap = null;
if (tokenObj == null) {
// 如果tokenMap为空
tokenMap = new HashMap<String, String>();
tokenMap.put(tokenKey, UUID.randomUUID().toString());
session.setAttribute("tokenMap", tokenMap);
} else if (tokenObj instanceof Map) {
// 如果tokenMap已经存在,就直接覆盖更新
tokenMap = (Map<String, String>) tokenObj;
tokenMap.put(tokenKey, UUID.randomUUID().toString());
}
if (tokenMap != null) {
request.setAttribute("token", tokenMap.get(tokenKey));
}
} /**
* <p>Token校验,比对客户端传过来的token是否在session中存在.</p>
* @since 2019年4月6日
*/
private void checkToken(HttpServletRequest request) {
// 判断客户端传过来的token是否在session的tokenMap中存在,存在则移除,不存在就是重复提交
String tokenKey = null;
HttpSession session = request.getSession(false);
if (session == null) {
throw new BusinessRuntimeException("当前会话已结束,请重新登录!");
}
Map<String, String> tokenMap = (Map<String, String>) session.getAttribute("tokenMap");
if (tokenMap != null && !tokenMap.isEmpty()) {
String clinetToken = request.getParameter("token");
if (clinetToken != null) {
Iterator<Map.Entry<String, String>> it = tokenMap.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
if (clinetToken.equals(entry.getValue())) {
tokenKey = entry.getKey();
break;
}
}
}
}
if (tokenKey == null) {
// 如果最终没有在Session中找到已存在的Key
throw new BusinessRuntimeException("当前页面已过期,请刷新页面后再试!或者您也可以在最后打开的页面中操作!");
}
}
}

在控制器的页面初始化方法和提交方法中配置注解

  1     @RequestMapping("replaceConfig.do")
2 @Token(save = true)
3 public ModelAndView replaceConfig(HttpServletRequest req, ReplaceVO input) throws Exception {
4 String changeNumber = req.getParameter("ContextName");
5 input.setChangeNumber(changeNumber);
6 input.setCurrentUserAccount(ContextUtil.getCurrentUser().getAccount());
7 if (BooleanUtils.isFalse(input.getIsAdmin())) {
8 input.setIsAdmin(ContextUtil.currentUserHasRole(BusinessConstants.ROLE_PLM_ADMIN));
9 }
10 return new ModelAndView("plm/replace/replaceConfig").addObject("vo", input);
11 }
12
13 @RequestMapping("saveReplace.do")
14 @ResponseBody
15 @Token(check = true)
16 public Result saveReplace(HttpServletRequest req, String jsonStr) throws Exception {
17 Result result = Result.getInstanceError();
18 ReplaceVO vo = JSON.parseObject(jsonStr, ReplaceVO.class);
19 try {
20 synchronized (lock) {
21 if (lock.contains(vo.getPitemNumber())) {
22 throw new BusinessRuntimeException("受影响物件存在未完成操作,请稍后再试!当前正在处理的受影响物件列表:" + lock);
23 } else {
24 lock.add(vo.getPitemNumber());
25 }
26 }
27 result = replaceService.saveReplace(vo);
28 } catch (BusinessRuntimeException e) {
29 result.setMsg(e.getMessage());
30 log.error("saveReplace_exception:" + e.getMessage());
31 } catch (Exception e) {
32 result.setMsg(ExceptionUtils.getRootCauseMessage(e));
33 log.error("saveReplace_exception_input:" + jsonStr, e);
34 } finally {
35 lock.remove(vo.getPitemNumber());
36 }
37 return result;
38 }

在页面隐藏域保存令牌,并在提交时将令牌传给服务端

<input type="hidden" id="token" name="token" value="${token}" />

效果:

如果用户又打开了一个相同的页面,服务器端令牌就会刷新,这时候再提交先前打开的页面就会报错。

如果将提交请求也设置为可以刷新令牌,那同样的请求提交2次就会报错。

    

  

  

控制请求重复提交的方法总结(Token)的更多相关文章

  1. php表单加入Token防止重复提交的方法分析

    http://www.jb51.net/article/94395.htm 这篇文章主要介绍了php表单加入Token防止重复提交的方法,结合实例形式分析了Token防止重复提交的原理与使用技巧,需要 ...

  2. PHP使用token防止表单重复提交的方法

    本文实例讲述了PHP使用token防止表单重复提交的方法.分享给大家供大家参考,具体如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 2 ...

  3. 防止刷新/后退引起的重复提交问题的Java Token代码,非Struts

    贴子转自http://hi.baidu.com/bobylou,转之前并没有验证文章里的方法是不是有效,估计原作者把它放到blog之前应该做过测试了吧. Struts本身有一套完善的防止重复提交表单的 ...

  4. web form 防止一个请求重复提交

    /// <summary> /// 防止一个请求重复提交 /// </summary> public void PreventRepeatSubmit() { if (Scri ...

  5. 防止Web表单重复提交的方法总结

    在Web开发中,对于处理表单重复提交是经常要面对的事情.那么,存在哪些场景会导致表单重复提交呢?表单重复提交会带来什么问题?有哪些方法可以避免表单重复提交? 表单重复提交的场景 1.场景一:服务端未能 ...

  6. php 解决和避免form表单重复提交的方法

    在提交表单的时候,可能遇到网速等导致页面突然加载变慢,用户重复地点击提交按钮,将在数据库产生多条数据,导致不可控情况. 比如下面的情况就会导致表单重复提交: 点击提交按钮两次. 点击刷新按钮. 使用浏 ...

  7. c#.net防止按F5刷新页面重复提交的方法

    在网上购物的过程中,提交完一个页面后,如果此时按f5刷新,则会弹出一个提示:如果继续,则会重新发送提交我们刚才提交的内容,这个问题应该规避掉,不然总是重复提交付款,那可不是件好事. 在c#.net中的 ...

  8. MVC Ajax.BeginForm重复提交解决方法

    mvc使用MVC Ajax.BeginForm提交的时候有重复提交结果的时候检查相关js文件引用情况, 其中mvc4注意 1 2 3 4 @Scripts.Render("~/bundles ...

  9. php 解决表单重复提交实现方法介绍

    重复提交是我们开发中会常碰到的一个问题,除了我们使用js来防止表单的重复提交,同时还可以使用php来防止重复提交哦. 例1  代码如下 复制代码 <?php/* * php中如何防止表单的重复提 ...

随机推荐

  1. linux 安装中文支持

    下载  fonts-chinese-3.02-12.el5.noarch.rpm fonts-ISO8859-2-75dpi-1.0-17.1.noarch.rpm 安装各种提示的依赖 安装 chkf ...

  2. springboot项目简单启动脚本

    #!/bin/bash function log_info () { DATE=`date "+%Y-%m-%d %H:%M:%S"` USER=$(whoami) echo &q ...

  3. 0011 删除链表的倒数第N个节点

    给 定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点. 示例: 给定一个链表: 1->2->3->4->5, 和 n = 2. 当删除了倒数第二个节点后,链表变为 ...

  4. bzoj4025二分图(线段树分治 并查集)

    /* 思维难度几乎没有, 就是线段树分治check二分图 判断是否为二分图可以通过维护lct看看是否链接出奇环 然后发现不用lct, 并查集维护奇偶性即可 但是复杂度明明一样哈 */ #include ...

  5. js实现选中div内容并复制到剪切板

    function copyUrl () { var div = document.getElementById('xxxx'); if (document.body.createTextRange) ...

  6. C++实现根据路径读取文件内容

    已知文件路径,用C++实现读取对应文件的内容,代码如下: bool LoadShaderStr(const char* szShaderPath,string& strShaderStr) { ...

  7. github 解决 Agent admitted failure to sign using the key

    公司迁移git 新库,重新迁移数据. 添加 ssh key  1. 首先要在新git 库管理平台 添加新的ssh-key : 本机上执行 ssh-keygen -t rsa -C "Your ...

  8. leetcode894

    class Solution { private Map<Integer, List<TreeNode>> memo; public List<TreeNode> ...

  9. Java好的的工具类:MD5

    import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class MD5 ...

  10. 一个suse11 sp1的crash工具版本问题

    这几年排查的各种类型的crash也比较多了,各种类型的也算见过,但是排查这个crash,走了不该走的弯路,事后显得很low,为了防止自己犯类似错误,也同时提醒后人,记录之. 内核是suse11,sp1 ...