java防止表单重复提交(实践版)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介:

在开发中,使用表单提交数据到服务器,经常会发生重复提交的情况。本文就重点说一下如何在开发中防止表单重复提交。

一、表单重复提交的常见应用场景

有如下的form.jsp页面

<%@ pagelanguage="java" import="java.util.*"pageEncoding="UTF-8"%>

<!DOCTYPEHTML>

<html>

  <head>

    <title>Form表单</title>

  </head>

 

  <body>

      <formaction="${pageContext.request.contextPath}/servlet/DoFormServlet"method="post">

        用户名:<input type="text"name="username">

        <input type="submit"value="提交"id="submit">

    </form>

  </body>

</html>

form表单提交到DoFormServlet进行处理

packagexdp.gacl.session;

 

importjava.io.IOException;

importjavax.servlet.ServletException;

importjavax.servlet.http.HttpServlet;

importjavax.servlet.http.HttpServletRequest;

importjavax.servlet.http.HttpServletResponse;

 

public classDoFormServlet extends HttpServlet {

 

    public void doGet(HttpServletRequestrequest, HttpServletResponse response)

            throws ServletException,IOException {

        //客户端是以UTF-8编码传输数据到服务器端的,所以需要设置服务器端以UTF-8的编码进行接收,否则对于中文数据就会产生乱码

       request.setCharacterEncoding("UTF-8");

        String userName =request.getParameter("username");

        try {

            //让当前的线程睡眠3秒钟,模拟网络延迟而导致表单重复提交的现象

            Thread.sleep(3*1000);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        System.out.println("向数据库中插入数据:"+userName);

    }

 

    public void doPost(HttpServletRequestrequest, HttpServletResponse response)

            throws ServletException,IOException {

        doGet(request, response);

    }

 

}

  如果没有进行form表单重复提交处理,那么在网络延迟的情况下下面的操作将会导致form表单重复提交多次

  1. 1.1、    场景一:在网络延迟的情况下让用户有时间点击多次submit按钮导致表单重复提交

对“提交”按钮进行禁用操作,即可避免。

1.2、场景二:表单提交后用户点击【刷新】按钮导致表单重复提交

点击浏览器的刷新按钮,就是把浏览器上次做的事情再做一次,这样也会导致表单重复提交。

1.3、场景三:用户提交表单后,点击浏览器的【后退】按钮回退到表单页面后进行再次提交

二、利用JavaScript防止表单重复提交

  既然存在上述所说的表单重复提交问题,那么我们就要想办法解决,比较常用的方法是采用JavaScript来防止表单重复提交,具体做法如下:

修改form.jsp页面,添加如下的JavaScript代码来防止表单重复提交

<%@ pagelanguage="java" import="java.util.*"pageEncoding="UTF-8"%>

<!DOCTYPEHTML>

<html>

  <head>

    <title>Form表单</title>

        <scripttype="text/javascript">

        var isCommitted = false;//表单是否已经提交标识,默认为false

        function dosubmit(){

            if(isCommitted==false){

                isCommitted = true;//提交表单后,将表单是否已经提交标识设置为true

                return true;//返回true让表单正常提交

            }else{

                return false;//返回false那么表单将不提交

            }

        }

    </script>

  </head>

 

  <body>

      <form action="${pageContext.request.contextPath}/servlet/DoFormServlet"onsubmit="return dosubmit()" method="post">

        用户名:<input type="text"name="username">

        <input type="submit"value="提交"id="submit">

    </form>

  </body>

</html>

 

我们看看使用了JavaScript来防止表单提交重复是否可以成功:

  可以看到,针对"在网络延迟的情况下让用户有时间点击多次submit按钮导致表单重复提交"这个应用场景,使用JavaScript是可以解决这个问题的,解决的做法就是"用JavaScript控制Form表单只能提交一次"。

  除了用这种方式之外,经常见的,也是作者推荐的另一种方式就是表单提交之后,将提交按钮设置为不可用,让用户没有机会点击第二次提交按钮,代码如下:

function dosubmit(){

    //获取表单提交按钮

    var btnSubmit =document.getElementById("submit");

    //将表单提交按钮设置为不可用,这样就可以避免用户再次点击提交按钮

    btnSubmit.disabled= "disabled";

    //返回true让表单可以正常提交

    return true;

}

   另外还有一种做法就是提交表单后,将提交按钮隐藏起来,这种做法和将提交按钮设置为不可用是差不多的,个人觉得将提交按钮隐藏影响到页面布局的美观,并且可能会让用户误以为是bug(怎么我一点击按钮,按钮就不见了呢?用户可能会有这样的疑问),我个人在开发中用得比较多的是表单提交后,将提交按钮设置为不可用,反正使用JavaScript防止表单重复提交的做法都是差不多的,目的都是让表单只能提交一次,这样就可以做到表单不重复提交了。

  使用JavaScript防止表单重复提交的做法只对上述提交到导致表单重复提交的三种场景中的【场景一】有效,而对于【场景二】和【场景三】是没有用,依然无法解决表单重复提交问题。

三、利用Session防止表单重复提交

  对于【场景二】和【场景三】导致表单重复提交的问题,既然客户端无法解决,那么就在服务器端解决,在服务器端解决就需要用到session了。

  具体的做法:在服务器端生成一个唯一的随机标识号,专业术语称为Token(令牌),同时在当前用户的Session域中保存这个Token。然后将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端,然后在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的标识号。
在下列情况下,服务器程序将拒绝处理用户提交的表单请求:

  1. 存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。

  2. 当前用户的Session中不存在Token(令牌)。

  3. 用户提交的表单数据中没有Token(令牌)。

看具体的范例:

  1.创建FormServlet,用于生成Token(令牌)和跳转到form.jsp页面

packagexdp.gacl.session;

 

import java.io.IOException;

importjavax.servlet.ServletException;

importjavax.servlet.http.HttpServlet;

importjavax.servlet.http.HttpServletRequest;

importjavax.servlet.http.HttpServletResponse;

 

public classFormServlet extends HttpServlet {

    private static final long serialVersionUID= -884689940866074733L;

 

    public void doGet(HttpServletRequestrequest, HttpServletResponse response)

            throws ServletException,IOException {

 

        String token =TokenProccessor.getInstance().makeToken();//创建令牌

        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(HttpServletRequestrequest, HttpServletResponse response)

            throws ServletException,IOException {

        doGet(request, response);

    }

 

}

  2.在form.jsp中使用隐藏域来存储Token(令牌)

<%@ pagelanguage="java" import="java.util.*"pageEncoding="UTF-8"%>

<!DOCTYPE HTMLPUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html>

<head>

<title>form表单</title>

</head>

 

<body>

    <formaction="${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>

</html>

  3.DoFormServlet处理表单提交

packagexdp.gacl.session;

 

importjava.io.IOException;

importjavax.servlet.ServletException;

importjavax.servlet.http.HttpServlet;

importjavax.servlet.http.HttpServletRequest;

importjavax.servlet.http.HttpServletResponse;

 

public classDoFormServlet extends HttpServlet {

 

    public void doGet(HttpServletRequestrequest, 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 booleanisRepeatSubmit(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(HttpServletRequestrequest, HttpServletResponse response)

            throws ServletException,IOException {

        doGet(request, response);

    }

 

}

  生成Token的工具类TokenProccessor

 

packagexdp.gacl.session;

 

importjava.security.MessageDigest;

importjava.security.NoSuchAlgorithmException;

importjava.util.Random;

importsun.misc.BASE64Encoder;

 

public classTokenProccessor {

 

    /*

     *单例设计模式(保证类的对象在内存中只有一个)

     *1、把类的构造函数私有

     *2、自己创建一个类的对象

     *3、对外提供一个公共的方法,返回类的对象

     */

    private TokenProccessor(){}

   

    private static final TokenProccessorinstance = new TokenProccessor();

   

    /**

     * 返回类的对象

     * @return

     */

    public static TokenProccessorgetInstance(){

        return instance;

    }

   

    /**

     * 生成Token

     * TokenNv6RRuGEVvmGjB+jimI/gw==

     * @return

     */

    public String makeToken(){  //checkException

        // 7346734837483 834u938493493849384  43434384

        String token =(System.currentTimeMillis() + new Random().nextInt(999999999)) + "";

        //数据指纹   128位长   16个字节  md5

        try {

            MessageDigest md =MessageDigest.getInstance("md5");

            byte md5[] =  md.digest(token.getBytes());

            //base64编码--任意二进制编码明文字符   adfsdfsdfsf

            BASE64Encoder encoder = newBASE64Encoder();

            return encoder.encode(md5);

        } catch (NoSuchAlgorithmException e) {

            throw new RuntimeException(e);

        }

    }

}

 

首先访问FormServlet,在FormServlet中生成Token之后再重定向到form.jsp页面,这次是在服务器端处理表单重复提交的:

从运行效果中可以看到,通过这种方式处理表单重复提交,可以解决上述的场景二和场景三中出现的表单重复提交问题。

以上内容参考博文:http://www.cnblogs.com/xdp-gacl/p/3859416.html

四、问题

需要注意的是:确保同一个用户进入同一个页面,session唯一;清空session时间,session过期时间处理!避免服务端压力过大!

 

多台服务器时,session不共享,导致提交不了表单问题!

解决方案:

1,  选择一个全局共享域可解决问题,比如:redis

2,  集群配置时,同一个用户访问始终指向同一个tomcat

 等等。

 

五、实战

FormTokenUtil工具类

FormTokenUtil工具类:包含了redis和session保存formToken的相关方法

 

单应用使用session即可解决问题,多台服务器可以使用全局redis作为储存中介。

 

 

packagecom.wupao.common.utils;

 

importjavax.servlet.http.HttpServletRequest;

 

importorg.apache.commons.codec.digest.DigestUtils;

importorg.apache.commons.lang3.RandomStringUtils;

importorg.apache.commons.lang3.StringUtils;

importorg.slf4j.Logger;

importorg.slf4j.LoggerFactory;

 

importcom.wupao.common.service.RedisService;

 

/**

 *

 * @项目名称:common

 * @类名称:FormTokenUtil

 * @类描述:防止form表单重复提交:生成随机formToken,校验formToken是否一致

 * @创建人:wyait

 * @创建时间:201789下午4:04:47

 * @version

 */

public classFormTokenUtil {

 

   private static final Logger LOGGER =LoggerFactory

        .getLogger(FormTokenUtil.class);

 

   private static final String FORM_TOKEN ="formToken:";

 

   // redistoken的保存时间:1天(1小时=3600s),因为登录用户有效期也是1

   private static final Integer TOKEN_SECODENS =86400;

   // 秒为单位

   private static final Integer SESSION_SECODENS= 1440 * 60;

 

   /**

    *

    * @描述:(redis)获取formToken

    * @创建人:wyait

    * @创建时间:201789下午4:08:57

    *@param uid

    *@return

    */

   public static StringgetFormToken(RedisService redisService, String uid) {

      if (StringUtils.isEmpty(uid) ||redisService == null) {

        return "";

      }

      // md5(当前毫秒值+随机6位数+uid)

      String formToken =DigestUtils.md5Hex(System.currentTimeMillis()

           + RandomStringUtils.randomNumeric(6)+ uid);

      LOGGER.debug("====获取formToken===wupao-common.FormTokenUtil.getFormToken()===formToken:"

           + formToken);

      // 保存在redis保存时间为24小时

      redisService.set(FORM_TOKEN + uid,formToken, TOKEN_SECODENS);

      return formToken;

   }

 

   /**

    *

    * @描述:(redis)校验formToken是否一致

    * @创建人:wyait

    * @创建时间:201789下午4:14:04

    *@param formToken

    *@param uid

    *@param redisService

    *@return true是可以提交,false重复提交

    */

   public static boolean checkFormToken(StringformToken, String uid,

        RedisService redisService) {

      // 校验数据

      if (StringUtils.isEmpty(formToken) ||StringUtils.isEmpty(uid)

           || redisService == null) {

        return false;

      }

      // 获取redis中的formToken

      String redisFormToken =redisService.get(FORM_TOKEN + uid);

      // 1,如果当前用户的redis中不存在formToken(令牌),则用户是重复提交了表单

      if (StringUtils.isEmpty(redisFormToken)) {

        return false;

      }

      // 2,存储在redis中的Token(令牌)与表单提交的Token(令牌)不同,则用户是重复提交了表单

      if (!formToken.equals(redisFormToken)) {

        return false;

      }

      return true;

   }

 

   /**

    *

    * @描述:删除redisformToken

    * @创建人:wyait

    * @创建时间:201789下午5:27:56

    *@param uid

    *@param redisService

    *@return

    */

   public static Long cleanFormToken(String uid,RedisService redisService) {

      // 校验数据

      if (StringUtils.isEmpty(uid) ||redisService == null) {

        return 0L;

      }

      return redisService.del(FORM_TOKEN + uid);

   }

 

   /**

    *

    * @描述:(session)获取formToken

    * @创建人:wyait

    * @创建时间:201789下午4:08:57

    *@param uid

    *@return

    */

   public static StringgetSessionFormToken(HttpServletRequest request,

        String uid) {

      if (StringUtils.isEmpty(uid)) {

        return "";

      }

      // md5(当前毫秒值+随机6位数+uid)

      String formToken =DigestUtils.md5Hex(System.currentTimeMillis()

           + RandomStringUtils.randomNumeric(6)+ uid);

      LOGGER.debug("====获取sessionformToken===wupao-common.FormTokenUtil.getFormToken()===formToken:"

           + formToken);

      // 保存在redis保存时间为24小时

      request.getSession().setAttribute(FORM_TOKEN+ uid, formToken);

      //request.getSession().setMaxInactiveInterval(SESSION_SECODENS);

      return formToken;

   }

 

   /**

    *

    * @描述:(session)校验formToken是否一致

    * @创建人:wyait

    * @创建时间:201789下午4:14:04

    *@param formToken

    *@param uid

    *@param request

    *@return true是可以提交,false重复提交

    */

   public static booleancheckSessionFormToken(String formToken, String uid,

        HttpServletRequest request) {

      // 校验数据

      if (StringUtils.isEmpty(formToken) ||StringUtils.isEmpty(uid)) {

        return false;

      }

      // 获取session中的formToken

      String sessionFormToken = (String)request.getSession().getAttribute(

           FORM_TOKEN + uid);

      // 1,如果当前用户的session中不存在formToken(令牌),则用户是重复提交了表单

      if (StringUtils.isEmpty(sessionFormToken)){

        return false;

      }

      // 2,存储在session中的Token(令牌)与表单提交的Token(令牌)不同,则用户是重复提交了表单

      if (!formToken.equals(sessionFormToken)) {

        return false;

      }

      return true;

   }

 

   /**

    *

    * @描述:删除sessionformToken

    * @创建人:wyait

    * @创建时间:201789下午5:27:56

    *@param uid

    *@param redisService

    */

   public static void delSessionToken(Stringuid, HttpServletRequest request) {

      // 校验数据

      if (StringUtils.isNotEmpty(uid)) {

        request.getSession().removeAttribute(FORM_TOKEN+ uid);

      }

   }

  

}

 

FormController中使用

FormController中使用:redis中保存formToken实战

 

1,添加formToken

// 添加formToken

        // 生成防止重复提交form表单的formToken(md5)

        String formTokenValue =FormTokenUtil.getFormToken(redisService,

              uid);

        LOGGER.debug(

              "====账户信息===wupao-vipweb.UserController.toUserInfo()===防止重复提交表单formTokenValue:【{}",

              formTokenValue);

        mv.addObject("formToken",formTokenValue);

 

2,校验formToken是否一致

// 校验是否重复提交表单

        if(!FormTokenUtil.checkFormToken(formToken, uid, redisService)) {

           LOGGER.debug(

                "====保存账户信息====wupao-vipweb.UserController.saveUser()===请求参数:wxmp{}】,响应结果:msg{}",

                 user, msg);

           mv.addObject("msg", "您未登录或登录超时,请重新登录");

           // 参数错误

           return mv;

        }

 

3,删除formToken

// 清空formToken

FormTokenUtil.cleanFormToken(uid,redisService);



本文转自 wyait 51CTO博客,原文链接:http://blog.51cto.com/wyait/1955046,如需转载请自行联系原作者

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
13天前
|
Java 调度
Java并发编程:深入理解线程池的原理与实践
【4月更文挑战第6天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将从线程池的基本原理入手,逐步解析其工作过程,以及如何在实际开发中合理使用线程池以提高程序性能。同时,我们还将关注线程池的一些高级特性,如自定义线程工厂、拒绝策略等,以帮助读者更好地掌握线程池的使用技巧。
|
29天前
|
安全 Java 调度
Java中的多线程编程:从理论到实践
【2月更文挑战第30天】本文旨在深入探讨Java中的多线程编程。我们将从基础的理论出发,理解多线程的概念和重要性,然后通过实际的Java代码示例,展示如何创建和管理线程,以及如何处理线程间的同步和通信问题。最后,我们还将讨论Java并发库中的一些高级特性,如Executor框架和Future接口。无论你是Java初学者,还是有经验的开发者,本文都将为你提供有价值的见解和实用的技巧。
|
1月前
|
XML Java 数据库连接
谈谈Java反射:从入门到实践,再到原理
谈谈Java反射:从入门到实践,再到原理
58 0
|
1月前
|
Java 程序员 索引
Java中的异常处理:理解、实践与最佳实践
【2月更文挑战第26天】在Java编程中,异常处理是一个重要的概念。它不仅帮助我们在程序出错时提供有关错误的详细信息,而且还允许我们以一种结构化的方式来处理这些错误。本文将深入探讨Java中的异常处理,包括如何创建自定义异常,如何使用try-catch-finally语句块,以及如何在实际编程中应用最佳实践。
26 3
|
14天前
|
Java 程序员 调度
Java中的多线程编程:基础知识与实践
【4月更文挑战第5天】 在现代软件开发中,多线程编程是一个不可或缺的技术要素。它允许程序员编写能够并行处理多个任务的程序,从而充分利用多核处理器的计算能力,提高应用程序的性能。Java作为一种广泛使用的编程语言,提供了丰富的多线程编程支持。本文将介绍Java多线程编程的基础知识,并通过实例演示如何创建和管理线程,以及如何解决多线程环境中的常见问题。
|
10天前
|
Java 数据挖掘
java实践
【4月更文挑战第9天】java实践
12 1
|
1天前
|
安全 Java 程序员
Java中的多线程并发编程实践
【4月更文挑战第18天】在现代软件开发中,为了提高程序性能和响应速度,经常需要利用多线程技术来实现并发执行。本文将深入探讨Java语言中的多线程机制,包括线程的创建、启动、同步以及线程池的使用等关键技术点。我们将通过具体代码实例,分析多线程编程的优势与挑战,并提出一系列优化策略来确保多线程环境下的程序稳定性和性能。
|
2天前
|
负载均衡 Java 开发者
细解微服务架构实践:如何使用Spring Cloud进行Java微服务治理
【4月更文挑战第17天】Spring Cloud是Java微服务治理的首选框架,整合了Eureka(服务发现)、Ribbon(客户端负载均衡)、Hystrix(熔断器)、Zuul(API网关)和Config Server(配置中心)。通过Eureka实现服务注册与发现,Ribbon提供负载均衡,Hystrix实现熔断保护,Zuul作为API网关,Config Server集中管理配置。理解并运用Spring Cloud进行微服务治理是现代Java开发者的关键技能。
|
2天前
|
Java API 数据库
深研Java异步编程:CompletableFuture与反应式编程范式的融合实践
【4月更文挑战第17天】本文探讨了Java中的CompletableFuture和反应式编程在提升异步编程体验上的作用。CompletableFuture作为Java 8引入的Future扩展,提供了一套流畅的链式API,简化异步操作,如示例所示的非阻塞数据库查询。反应式编程则关注数据流和变化传播,通过Reactor等框架实现高度响应的异步处理。两者结合,如将CompletableFuture转换为Mono或Flux,可以兼顾灵活性和资源管理,适应现代高并发环境的需求。开发者可按需选择和整合这两种技术,优化系统性能和响应能力。
|
2天前
|
网络协议 Java API
深度剖析:Java网络编程中的TCP/IP与HTTP协议实践
【4月更文挑战第17天】Java网络编程重在TCP/IP和HTTP协议的应用。TCP提供可靠数据传输,通过Socket和ServerSocket实现;HTTP用于Web服务,常借助HttpURLConnection或Apache HttpClient。两者结合,构成网络服务基础。Java有多种高级API和框架(如Netty、Spring Boot)简化开发,助力高效、高并发的网络通信。