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

摘要:
在Web开发中,经常需要处理表单的重复提交。如何避免重复提交表单关于解决重复提交表单的问题,有两种方式:前端拦截和服务器端拦截。“);returnfalse;}通过禁用按钮来阻止。除了通过在前端设置标志位来阻止之外,您还可以在表单提交后禁用按钮,这完全防止了表单被重新提交的可能性。虽然在前端阻止可以解决方案1中表单重新提交的问题,但无法阻止表单重新提交在场景2(刷新)和场景3中提交。

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

表单重复提交的场景

1.场景一:服务端未能及时响应结果(网络延迟,并发排队等因素),导致前端页面没有及时刷新,用户有机会多次提交表单
用户重复提交表单

2.场景二:提交表单成功之后用户再次点击刷新按钮导致表单重复提交
提交成功之后刷新重复提交

3.场景三:提交表单成功之后点击后退按钮回退到表单页面再次提交
回退到表单页面再次提交

表单重复提交的弊端

下面通过一个简单的示例进行说明。

  • 表单页面: test-form-submit-repeat.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>处理表单重复提交</title>
</head>
<body>

    <form action="<%=request.getContextPath()%>/formServlet.do" method="post">
        姓名:<input type="text" name="username" />
        <input type="submit" value="提交">
    </form>
</body>
</html>
  • 后台Serlvet:FormServlet.java
public class FormServlet extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("UTF-8");
        String userName = req.getParameter("username");

        try {
            // 模拟服务端处理效率慢
            Thread.sleep(3 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("插入数据:" + userName);
    }
}

实验表单重复提交结果:
实验表单重提交

显然,从演示结果来看,如果出现表单重复提交,将会导致相同的数据被重复插入到数据库中。实际上,这是不应该发生的。

如何避免重复提交表单

关于解决表单重复提交,分为在前端拦截和服务端拦截2种方式。

1.在前端对表单重复提交进行拦截

在前端拦截表单重复提交可以通过多种方式实现:
(1)通过设置变量标志位进行拦截

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>处理表单重复提交</title>
</head>
<body>
    <form action="<%=request.getContextPath()%>/formServlet.do" method="post" onsubmit="return checkSubmit();">
        姓名:<input type="text" name="username" />
        <input type="submit" value="提交">
    </form>
</body>
<script type="text/javascript">
    // 设置表单提交标志
    var submit = false;
    function checkSubmit() {
        if(!submit) {
            // 表单提交后设置标志位
            submit = true;
            return true;
        }
        // 表单已经提交,不允许再次提交
        console.log("请不要重复提交表单!");
        return false;
    }
</script>
</html>

在前端设置变量标志拦截表单重复提交

(2)通过禁用按钮进行拦截
除了在前端通过设置标志位进行拦截之外,还可以在表单提交之后将按钮disabled掉,这样就彻底阻止了表单被重复提交的可能。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>处理表单重复提交</title>
</head>
<body>
    <form action="<%=request.getContextPath()%>/formServlet.do" method="post" onsubmit="return disabledSubmit();">
        姓名:<input type="text" name="username" />
        <input   type="submit" value="提交">
    </form>
</body>
<script type="text/javascript">
    function disabledSubmit() {
        // 在提交按钮第一次执行之后就disabled掉,避免重复提交
        document.getElementById("submitBtn").disabled= "disabled";
        return true;
    }
</script>
</html>

提交表单之后disabled掉按钮

当然,还可以直接在提一次提交之后将按钮隐藏掉。但是,是否需要这样做,需要考虑用户的操作体验是不是可以接受。

在前端拦截虽然可以解决场景一的表单重复提交问题,但是针对场景二(刷新)和场景三(后退重新提交)的表单重复提交是无能为力的。
前端拦截只对场景一有效

2.在服务器端对表单重复提交进行拦截
在服务器端拦截表单重复提交的请求,实际上是通过在服务端保存一个token来实现的,而且这个在服务端保存的token需要通过前端传递,分三步走:

第一步:访问页面时在服务端保存一个随机token

public class FormServlet extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        UUID uuid = UUID.randomUUID();
        String token = uuid.toString().replaceAll("-", "");
        // 访问页面时随机生成一个token保存在服务端session中
        req.getSession().setAttribute("token", token);
        req.getRequestDispatcher("/test-form-submit-repeat.jsp").forward(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
       doGet(req, resp);
    }
}

随机token的产生可以使用任何恰当的方式,在这里通过UUID产生。

第二步:将服务端端保存的随机token通过前端页面传递

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>处理表单重复提交</title>
</head>
<body>
    <form action="<%=request.getContextPath()%>/doFormServlet.do" method="post">
        <!-- 隐藏域保存服务端token -->
        <input type="hidden" name="token" value="<%=session.getAttribute("token")%>" />
        姓名:<input type="text" name="username" />
        <input   type="submit" value="提交">
    </form>
</body>
</html>

第三步:提交表单时在服务端通过检查token来判断是否为重复提交的表单请求

public class DoFormServlet extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("UTF-8");

        if(checkRepeatSubmit(req)) {
            System.out.println("请不要重复提交!");
            return;
        }
        
        // 在第一次处理表单之后需要清空token,这一步非常关键
        req.getSession().removeAttribute("token");

        String userName = req.getParameter("username");
        try {
            // 模拟服务端处理效率慢
            Thread.sleep(3 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("插入数据:" + userName);
    }

    // 检查表单是否为重复提交
    private boolean checkRepeatSubmit(HttpServletRequest req) {
        Object sessionTokenObj = req.getSession().getAttribute("token");
        if(sessionTokenObj == null) {
            // 表单重复提交
            System.out.println("Session token is NULL!");
            return true;
        }

        String paramToken = req.getParameter("token");
        if(paramToken == null) {
            // 非法请求
            System.out.println("Parameter token is NULL!");
            return true;
        }

        if(!paramToken.equals(sessionTokenObj.toString())) {
            // 非法请求
            System.out.println("Token not same");
            return true;
        }
        return false;
    }
}

在服务端拦截表单重复提交

显然,通过在服务端保存token的方式拦截场景二和场景三的表单重复提交是非常有效的。而且,这种方式同样可以拦截场景一的表单重复提交。
通过服务器端拦截的方式解决场景一的表单重复提交

也就是说,对于拦截表单重复提交的终极解决方案是在服务器端进行拦截!不过,考虑到用户操作体验的问题,可能需要同时在前端进行拦截,这可以根据具体的产品设计而定。
在服务端拦截表单重复提交的原理

另外,有意思的是:在最新的Firefox浏览版本(Firefox Quantum 59.0.1 64位)中,浏览器自己就能处理场景一的表单重复提交(但是不能处理场景二和场景三的表单重复提交)。
火狐浏览器处理表单重复提交
经过验证,在最新版的Chrome(Chrome 65.0.3325.181)浏览器中还不具备这个功能。

免责声明:文章转载自《防止Web表单重复提交的方法总结》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇SSIS 处理错误的方法SxsTrace工具使用方法下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

egg.js的egg-jwt生成token做成中间件

做egg.js项目时想生成token,用egg-jwt插件最好了,但是不小心很容易踩到坑。主要主要几点: 1、把csrf关掉   config.security ={ csrf: { enable: false, ignoreJSON: true} }; 2、验证token写成中间件 3、前端交互时在headers传toke...

vue.js / nuxt.js 微信公众号判断是否在微信浏览器中打开并授权

首先, 看到这个需求, 应该知道这段代码要放在路由守卫中, 每次路由的变化都要触发这个校验 然后...话不多说, 看代码吧 // afterEach有待商榷, 我觉得beforeEach应该会更好, 是不是刚进项目时, beforeEach有点问题? 我试试后再追加. app.router.afterEach(async (to,from)=>{...

web 阶段的一些简答题

1.jsp 9个隐含对象 2. jsp 4大域对象 3.mybatis 中 #{} %{ } 的区别于联系 4. Servlet容器默认是采用单实例多线程的方式处理多个请求的: 5.Cookie 与Session 的异同 6. 请描述对web 服务器的理解(Tomcat),请列举出tomcat7下的目录以及各个目录的作用 7. 请描述 servlet与st...

ASP.NET 中实现会话状态的基础

简介 在 Web 应用程序这样的无状态环境中,了解会话状态的概念并没有实际的意义。尽管如此,有效的状态管理对于大多数 Web 应用程序来说都是一个必备的功能。Microsoft® ASP.NET 以及许多其他服务器端编程环境都提供了一个抽象层,允许应用程序基于每个用户和每个应用程序存储持久性数据。 需要特别注意的是,Web 应用程序的会话状态是应用程序在不...

SharePoint 2013 图文开发系列之InfoPath入门

本文主要介绍SharePoint 2013中,简单发布InfoPath表单,并添加后台代码,示例比较简单,主要描述的是一个创建InfoPath的过程,而非多么深奥的后台代码,希望能够给初学者带来帮助。 主要过程有 Ø新建一个InfoPath表单 Ø修改表单的信任级别并添加证书 Ø发布到InfoPath得到管理员认证 Ø设计InfoPath布局及添加...

简化Web开发的12个HTML5CSS框架

HTML5已经在Web开发中越来越流行。并且现在大部分流行的浏览器包括Firefox 6, Google Chrome, IE9等都支持HTML5。 利用框架能够帮助Web开发人员快速进行设计和开发。一个HTML5的框架提供了许多功能,如优美的排版,视频播放器,表单验证等,使开发人员能够轻松地 开发Web应用程序。 1. 52 Framework : HT...