path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path ;% > Title /toServlet01" method ="post" > 用户名: input type ="text" name ="userName" > input typ" />
0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何解决表单重复提交的问题

科技绿洲 来源:Java技术指北 作者:Java技术指北 2023-10-09 15:57 次阅读

关于表单的提交相信作为一个后端开发接触过不少,本文将介绍如何解决表单重复提交的问题。

1、表单提交案例

我们通过一个 jsp 页面提交表单到 servlet 进行处理。项目结构如下:图片
首先看 JSP 页面:from01.jsp

< %@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"% >
< %
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
            + path;
% >
< !DOCTYPE html >
< head >
    < title >Title< /title >
< /head >
< body >

    < form action="< %=basePath% >/toServlet01" method="post" >
        用户名:< input type="text" name="userName" >
        < input type="submit" value="提交" id="submit" >
    < /form >
< /body >
< /html >

接着我们看 servlet 操作:

package com.ys.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Create by YSOcean
 */
@WebServlet("/toServlet01")
public class FormServlet01 extends HttpServlet{
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("userName");
        try {
            //模拟网络延时
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("提交表单");
        resp.setContentType("text/html;charset=UTF-8");
        resp.getWriter().print("提交成功!!!");
    }
}

我们将该项目部署到 tomcat 服务器,然后启动服务器,在浏览器中输入相应地址,点击表单中的提交按钮,后台正常情况下应该打印出提交表单的字样,然后前台页面输出提交成功。

图片

2、表单重复提交的三种情况

上面我们演示的是正常点击提交的情况,但是实际上用户可能进行多次提交的操作。

①、多次点击提交按钮

这是最明显的一种情况,可能由于我们点击一次按钮后,系统后台对提交操作进行处理有一定的延时,于是页面停在表单提交页面。而当前用户不知道,以为没有提交表单,于是又进行按钮点击,造成表单多次提交。

图片

②、用户提交表单成功之后不断点击浏览器【刷新】按钮

图片

③、提交表单成功后,点击浏览器【回退】箭头,回到表单提交页面,然后重新点击提交按钮

图片

3、前端解决办法

①、onsubmit() 方法

在表单中增加onsubmit() 方法,该方法在表单提交时触发,返回false时,表单就不会被提交。针对用户多次点击按钮提交的问题,我们在前端控制表单提交一次之后,将 onsubmit() 方法返回值改为false,那么第二次点击提交按钮,表单将不能进行提交。

< %@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"% >
< %
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
            + path;
% >
< !DOCTYPE html >
< head >
    < title >Title< /title >
< /head >
< script type="text/javascript" >
    var isFlag = false;
    function dosubmit(){
        if(!isFlag){
            isFlag = true;
            return true;
        }else{
            return false;
        }
    }

< /script >
< body >
    < form action="< %=basePath% >/toServlet01" method="post" onsubmit="return dosubmit()" >
        用户名:< input type="text" name="userName" >
        < input type="submit" value="提交" id="submit" >
    < /form >
< /body >
< /html >

图片

②、表单提交之后,将按钮设置不可点击

function dosubmit(){
        //获取表单提交按钮
        var btnSubmit = document.getElementById("submit");
        //将表单提交按钮设置为不可用,这样就可以避免用户再次点击提交按钮
        btnSubmit.disabled= "disabled";
        //返回true让表单可以正常提交
        return true;
    }

存在问题:前面这两种方法只能应对用户多次点击提交按钮的情况,也就是上面的第一种情况。但是对于提交之后多次刷新以及点击回退按钮,再次提交的这两种情况却没有效果。这时候就需要在后端进行解决。

4、后端解决

具体做法:

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

在下列情况下,服务器程序将拒绝处理用户提交的表单请求:

1、存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。(包括伪造Token)

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

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

①、首先通过服务器端的 servlet 跳转到表单提交页面:

package com.ys.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;

/**
 * Create by YSOcean
 */
@WebServlet("/toForm")
public class ToFromServlet extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String tokenId = UUID.randomUUID().toString();
        req.getSession().setAttribute("tokenId",tokenId);
        req.getRequestDispatcher("from01.jsp").forward(req,resp);
    }
}

②、表单页面增加隐藏域存储tokenId

< %@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8" isELIgnored="false"% >
< %
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
            + path;
% >
< !DOCTYPE html >
< head >
    < title >Title< /title >
< /head >
< script type="text/javascript" >
    var isFlag = false;
    /*function dosubmit(){
        if(!isFlag){
            isFlag = true;
            return true;
        }else{
            return false;
        }
    }*/
    function dosubmit(){
        //获取表单提交按钮
        var btnSubmit = document.getElementById("submit");
        //将表单提交按钮设置为不可用,这样就可以避免用户再次点击提交按钮
        btnSubmit.disabled= "disabled";
        //返回true让表单可以正常提交
        return true;
    }

< /script >
< body >
    < form action="< %=basePath% >/toServlet01" method="post" onsubmit="return dosubmit()" >
        < input type="hidden" name="tokenId" value="${tokenId}" >
        用户名:< input type="text" name="userName" >
        < input type="submit" value="提交" id="submit" >
    < /form >
< /body >
< /html >

③、提交表单,后端进行是否重复判断

package com.ys.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Create by YSOcean
 */
@WebServlet("/toServlet01")
public class FormServlet01 extends HttpServlet{
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=UTF-8");

        String username = req.getParameter("userName");
        Boolean flag = isRepeatSubmit(req);
        if(flag){
            resp.getWriter().print("请不要重复提交!!!");
            return;
        }
        try {
            //模拟网络延时
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("提交表单");

        resp.getWriter().print("提交成功!!!");
    }

    private boolean isRepeatSubmit(HttpServletRequest request){
        //1、获取存储在request域中的tokenId
        String req_tokenId = request.getParameter("tokenId");
        //req_tokenId == null 表示表单中没有token,即用户不是通过servlet跳转到该页面或者是重复提交
        if(req_tokenId == null) {
            return true;
        }

        //2、获取存储在session域中的tokenId
        String session_tokenId = (String) request.getSession().getAttribute("tokenId");
        //如果当前session域中的tokenId为null,则表示用户重复提交(每次提交之后会移除该session域中的tokenId)
        if(session_tokenId == null){
            return true;
        }

        //3、存储在session域中的tokenId和表单隐藏域保存提交的tokenId不同,则表示用户伪造tokenId或者重复提交
        if(!session_tokenId.equals(req_tokenId)){
            return true;
        }
        //移除session域中的tokenId
        request.getSession().removeAttribute("tokenId");
        return false;
    }
}

图片

上面主要是利用一次回话中session域存储的数据是保持不变的,而request域只能保存一次请求的数据。

注意:页面首先要通过 servlet 进行跳转过去,不能直接访问jsp页面。先在 servlet 中生成一个 tokenId,然后将tokenId存入到session域中,在转发到jsp表单页面,在表单页面中,通过隐藏域存放生成的tokenId,然后点击提交按钮,会将隐藏域的tokenId 也一起提交到后端。后端首先判断表单中的tokenId值,以及和session域中的tokenId 值进行对比,表单中的tokenId为null,则说明是直接访问的jsp页面,session域中的tokenId 为null,则说明不是第一次提交,因为第一次提交成功之后会清空session域中的tokenId。都不为null,且两者不相等,则说明可能是伪造的tokenId;不为null,且相等,则说明是第一次提交。

这里要注意销毁session域中的tokenId时机,是在判断完是否重复提交的方法中最后就销毁了,这样可以防止还没销毁session域中的tokenId,客户端的请求又来了。

5、session共享问题

通过上面前后端的解决表单重复提交的问题,我们看似解决了,其实不然,对于各种分布式项目,为了解决高并发的问题,我们会将前端请求通过 nginx 负载到多个tomcat服务器,如下:

图片

这里会存在这样一个问题:

首先通过 tomcat1 将请求跳转到表单页面,这时候tokenId 是存放在tomcat1 session域中,然后点击提交按钮,nginx 可能会将我们的请求分发到 tomcat2 上,而tomcat2 的session 域中是不存在 tokenId 的,这时候我们提交不了表单。

这也是session共享问题。也就是说我们必须找到一个存放 tokenId 的公共介质,无论是哪个服务器去处理请求,都是从公共介质中获取 tokenId,那么当然不会存在tokenId 不一致的问题。

解决办法:

①、利用数据库同步:也就说将 tokenId 存放在数据库中,每次获取的时候从数据库中查询,这能解决,但是对数据的访问压力增大,不太合适。

②、利用 cookie 同步:因为 cookie 是存在本地客户端的,第一次请求我们将tokenId 存放在cookie中,然后从cookie进行是否重复提交校验,这也能解决问题。但是cookie 存在安全性问题,而且每次http请求都要带上参数也增加了带宽消耗。

③、利用 Redis 同步:这是最好的一种办法,Redis是一个高性能缓存框架,我们将 tokenId 存放在Redis中,获取也从Redis中获取,而且Redis性能极佳。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 服务器
    +关注

    关注

    12

    文章

    8120

    浏览量

    82522
  • 开发
    +关注

    关注

    0

    文章

    343

    浏览量

    40607
  • JSP
    JSP
    +关注

    关注

    0

    文章

    27

    浏览量

    10308
  • Servlet
    +关注

    关注

    0

    文章

    18

    浏览量

    7865
收藏 人收藏

    评论

    相关推荐

    [求助]提交表单代码话题!谁能解决我这断代码的问题.我真会千谢万谢.

    提交表单代码话题!谁能解决我这断代码的问题.我真会千谢万谢.<FORM name=formXueLi >   
    发表于 09-09 18:32

    报名提交文档出错,又重新报名了可以吗

    由于报名格式没搞清楚,结果报名时候提交了错误格式的项目方案,现在已经重复报名了,如何解决~~~~~
    发表于 11-08 14:43

    有没有stm32 做client 向web server使用http请求提交表单的例子...

    最近刚开始入手stm32f103c8,发现所有的网络相关的例子都是stm32 做server。请问,有没有stm32 做client 向web server使用http请求提交表单的例子呢?求指导谢谢
    发表于 03-27 17:16

    重复表单问题

    嗨,我需要帮助。我有一个主窗体,在单击按钮时会打开其他窗体。问题是当另一个表单打开时,它会在第二次单击按钮后再次打开。因此,它将使重复表单出现在桌面上。当我第一次点击按钮打开表单时,
    发表于 03-25 09:59

    camunda嵌入式表单的相关资料下载

    https://docs.camunda.org/manual/latest/user-guide/task-forms/#camunda-forms首先,camunda平台的表单分为三种:1
    发表于 12-14 08:51

    嵌入式表单的相关资料推荐

    嵌入式表单的介绍关键词:工作流表单方案 表单自定义 java工作流引擎 工作流设计 定义概述:一个已经做好的表单需要绑定到节点上。自定义表单
    发表于 12-17 06:24

    HarmonyOS实现表单页面的输入,必填校验和提交

    一. 样例介绍 本篇Codelab基于input组件、label组件和dialog组件,实现表单页面的输入、必填校验和提交: 为input组件设置不同类型(如:text,email,date等
    发表于 09-05 14:34

    基于SSH框架的动态表单设计与实现

    目前Web 应用系统中用户对表单的需求不断变化,因此需要一种动态、灵活、安全、快速有效的表单设计方法以方便系统管理和维护。介绍如何运用J2EE 的SSH 开源框架设计出一种动态表单
    发表于 09-13 17:01 42次下载
    基于SSH框架的动态<b class='flag-5'>表单</b>设计与实现

    JAVA教程之简单的表单程序

    JAVA教程之简单的表单程序,很好的学习资料。
    发表于 03-31 11:13 6次下载

    教你一招,如何解决路由重复问题

    在连接内网的网卡上设置:ip route 192.168.0.0 255.255.254.0 192.168.0.253。 在连接外网的网卡上设置:ip route 0.0.0.0 0.0.0.0 218.200.200.193 (与交换机interface ve 10路由重复
    发表于 09-28 14:37 4992次阅读
    教你一招,如<b class='flag-5'>何解</b>决路由<b class='flag-5'>重复</b>问题

    怎样在Visual Basics中制作登录表单

    添加第二个表单(当您输入正确的信息时,登录表单将带您进入什么),然后完成登录表单
    的头像 发表于 11-22 11:39 2358次阅读
    怎样在Visual Basics中制作登录<b class='flag-5'>表单</b>

    FMFormSubmitKit iOS表单提交

    ./oschina_soft/FMFormSubmitKit.zip
    发表于 06-23 10:57 0次下载
    FMFormSubmitKit iOS<b class='flag-5'>表单</b><b class='flag-5'>提交</b>

    Python如何解决无重复字符的最长子串问题

    这是一个关于字符串的经典问题,给定一个字符串,求出其中最长的不含有重复字符的子串。例如,给定字符串 `abcabcbb`,则其中最长的不含重复字符的子串为 `abc`,长度为 `3`。
    的头像 发表于 03-03 14:34 1020次阅读

    redis锁incres防止重复提交

    。Redis的原子性操作和分布式锁机制提供了一种解决方案,通过使用Redis的INCR命令和锁机制,可以防止重复提交。 一、Redis的原子性操作和INCR命令 在多线程或分布式环境下,多个请求可能同时对同一个计数器进行操作,如果不使用原子性操作,就
    的头像 发表于 12-04 13:50 257次阅读

    为什么要实现幂等性校验 如何实现接口的幂等性校验

    前端重复提交表单:在填写一些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,致使用户认为没有成功
    的头像 发表于 02-20 14:14 815次阅读