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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容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
相关文章
|
18天前
|
人工智能 自然语言处理 前端开发
从理论到实践:使用JAVA实现RAG、Agent、微调等六种常见大模型定制策略
大语言模型(LLM)在过去几年中彻底改变了自然语言处理领域,展现了在理解和生成类人文本方面的卓越能力。然而,通用LLM的开箱即用性能并不总能满足特定的业务需求或领域要求。为了将LLM更好地应用于实际场景,开发出了多种LLM定制策略。本文将深入探讨RAG(Retrieval Augmented Generation)、Agent、微调(Fine-Tuning)等六种常见的大模型定制策略,并使用JAVA进行demo处理,以期为AI资深架构师提供实践指导。
163 73
|
4月前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
72 6
|
4月前
|
设计模式 Java 开发者
Java中的异常处理:理解与实践
【10月更文挑战第42天】在Java的世界中,异常处理是每个开发者必须面对的挑战。它就像是一场不可预知的风暴,可能会在任何时候突然降临,打乱我们的计划。但是,如果我们能够掌握正确的处理方法,这场风暴也可以变成推动我们前进的力量。本文将带你深入理解Java中的异常处理机制,通过代码示例,我们将一起学习如何捕获、处理和预防异常,让你的程序在面对任何挑战时都能保持稳健和优雅。
|
18天前
|
Arthas 监控 Java
拥抱 OpenTelemetry:阿里云 Java Agent 演进实践
拥抱 OpenTelemetry:阿里云 Java Agent 演进实践
|
4月前
|
Arthas 监控 Java
拥抱 OpenTelemetry:阿里云 Java Agent 演进实践
本文介绍了阿里云 Java Agent 4.x 版本在基于 OTel Java Agent 二次开发过程中的实践与思考,并重点从功能、性能、稳定性、兼容性四个方面介绍了所做的工作。同时也介绍了阿里云可观测团队积极参与开源建设取得的丰厚成果。
492 10
拥抱 OpenTelemetry:阿里云 Java Agent 演进实践
|
2月前
|
Kubernetes Java 持续交付
小团队 CI/CD 实践:无需运维,Java Web应用的自动化部署
本文介绍如何使用GitHub Actions和阿里云Kubernetes(ACK)实现Java Web应用的自动化部署。通过CI/CD流程,开发人员无需手动处理复杂的运维任务,从而提高效率并减少错误。文中详细讲解了Docker与Kubernetes的概念,并演示了从创建Kubernetes集群、配置容器镜像服务到设置GitHub仓库Secrets及编写GitHub Actions工作流的具体步骤。最终实现了代码提交后自动构建、推送镜像并部署到Kubernetes集群的功能。整个过程不仅简化了部署流程,还确保了应用在不同环境中的稳定运行。
101 9
|
3月前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
3月前
|
安全 Java 数据库连接
Java中的异常处理:理解与实践
在Java的世界里,异常处理是维护代码健壮性的守门人。本文将带你深入理解Java的异常机制,通过直观的例子展示如何优雅地处理错误和异常。我们将从基本的try-catch结构出发,探索更复杂的finally块、自定义异常类以及throw关键字的使用。文章旨在通过深入浅出的方式,帮助你构建一个更加稳定和可靠的应用程序。
52 5
|
4月前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
506 6
|
3月前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。