html和ajax请求, 防止表单重复提交

本文前部分内容是转载的,后半部是自己写的. 转载地址点这里: 原作者地址

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

表单重复提交的场景

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

html和ajax请求, 防止表单重复提交_第1张图片

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

html和ajax请求, 防止表单重复提交_第2张图片

3.场景三:提交表单成功之后点击后退按钮回退到表单页面再次提交
html和ajax请求, 防止表单重复提交_第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);
    }
}

实验表单重复提交结果:
html和ajax请求, 防止表单重复提交_第4张图片

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

如何避免重复提交表单

关于解决表单重复提交,分为在前端拦截和服务端拦截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>

html和ajax请求, 防止表单重复提交_第5张图片
(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 id="submitBtn" type="submit" value="提交">
    </form>
</body>
<script type="text/javascript">
    function disabledSubmit() {
     
        // 在提交按钮第一次执行之后就disabled掉,避免重复提交
        document.getElementById("submitBtn").disabled= "disabled";
        return true;
    }
</script>
</html>

html和ajax请求, 防止表单重复提交_第6张图片
当然,还可以直接在提一次提交之后将按钮隐藏掉。但是,是否需要这样做,需要考虑用户的操作体验是不是可以接受。

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

html和ajax请求, 防止表单重复提交_第7张图片

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 id="submitBtn" 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的方式拦截场景二和场景三的表单重复提交是非常有效的。而且,这种方式同样可以拦截场景一的表单重复提交。


在我的开发中,表单提交试用的是ajax提交,而且提交页面,不是jsp,而是html页面.这时候该怎么办呢?

  1. 第一步是在html的表单中设置隐藏域token,但是Token的值没办法和jsp一样直接从域对象中获取,我们要发送ajax请求的时候获取

<html lang="en">
<head>
    <meta charset="utf-8">
    <title>注册title>
    
    <script src="js/jquery-3.3.1.js">script>
head>
<body>
            <form id="registerForm" action="user">
                   <input type="hidden" id="token" name="Token" value="">
                            <label for="username">用户名label>
                            <input type="text" id="username" name="username" autocapitalize="off" placeholder="请输入账号">
                            <label for="password">密码label>
                            <input type="password" id="password" name="password" placeholder="请输入密码">
          form>
        
<script src="js/jquery.validate.min.js">script>
<script>
    //防止表单重复提交,从服务端的session中获取Token,放在隐藏域中,提交到客户端
    $.post("user","action=getToken",function (result) {
      
         $("#token").val(result);
    },"text");

    //提交表单的验证和注册
    $("#registerForm").validate({
      
        //表单本身的提交被阻止,通过验证后,会执行这个函数,在这个函数里发送异步请求
        submitHandler: function (form) {
      
            //把表单的数据序列化,form表示当前表单对象
            var params = $(form).serialize();
            //发送ajax异步请求
                $.post("user", params, function (result) {
      
                    if (result.ok) {
      
                        //注册成功,跳转到成功页面
                        location.href = "success.html";
                    } else {
      
                        //注册失败,提示错误信息
                        alert(result.msg);
                    }
                }, "json");
        },
        rules: {
      
            username: {
      
                required: true,
                rangelength: [6, 18]
            },
            password: {
      
                required: true,
                rangelength: [6, 12]
            }
        },
        messages: {
      
            username: {
      
                required: "请输入用户名",
                rangelength: "用户名最少6位"
            },
            password: {
      
                required: "请输入密码",
                rangelength: "密码最少6位"
            }
        }
    });
script>
body>
html>
  1. 在服务端接受参数,和session中的Token比较,如果一样就是第一次提交,并且在提交成功的时候必须把session中的Token清空
public void getToken(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
     
        String token = TokenUtils.getInstance().makeToken();
        System.out.println("第一次生成的token:" + token);
        //服务端生成的Token保存在session中
        request.getSession().setAttribute("Token", token);
        String sessionToken = (String) request.getSession().getAttribute("Token");
        //发送给客户端
        if (sessionToken == null) {
     
            response.getWriter().write("");
        } else {
     
            response.getWriter().write(sessionToken);
        }
    }
 private boolean checkToken(HttpServletRequest request) {
     
        String sessionToken = (String) request.getSession().getAttribute("Token");
        if (sessionToken == null || "".equals(sessionToken)) {
     
            System.out.println("服务端的token是空,表单重复提交");
            return true;
        }
        String paramToken = request.getParameter("Token");
        if (paramToken == null || "".equals(paramToken)) {
     
            System.out.println("非法请求,客户端的token是空");
            return true;
        }
        if (!paramToken.equals(sessionToken)) {
     
            System.out.println("非法请求,Token的值不一样");
            return true;
        }
        //其他情况都是第一次提交表单
        return false;
    }
    public void register(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
     
        
                //先处理表单重复提交
                if (checkToken(request)) {
     
                //如果是重复提交,直接中断该请求
                    return;
                }
                //第一次提交之后,必须清空Token,这一步很重要
                request.getSession().removeAttribute("Token");
                //之后就是对表单的业务处理了....
    }

总结:

我们只需要生成一个唯一的token,分别放进客户端的表单里和服务器的session中进行了。当我们发起请求时,只需要判断session中的token(以下简称serverToken)和客户端表单里的token(以下简称clientToken)是否相等。 如果severToken是null, clientToken是null 还有serverToken不等于clientToken,那么就说明表单被重复提交了。反之,如果serverToken==clientToken,就说明表单没有被重复提交,当我们进行了一系列需要的操作后,就可以清除session中的token了。

扩展:

  1. 当一个浏览器一个用户同时开启2个页面假设是A和B,A提交之后,B提交会失败,
    因为在打开A和B页面的时候,同一个session中生成了2个Token,而且Token同名,前一个Token会被后面的覆盖,
    会导致先加载的页面提交的表单被阻止.如何解决这个问题?
    解决方案很简单: 就是在给session添加Token的时候,把key的值每次都设置的不一样
		//生成随机的Token,TokenUtils是我只写的工具类
      String token = TokenUtils.getInstance().makeToken();
        //生成随机的session的key,原来存储token
        String session_token_key = TokenUtils.getInstance().makeToken();
        //Token保存在session中,key和value都是随机的
       //之前的写法: request.getSession().setAttribute("Token", token);
       request.getSession().setAttribute(session_token_key, token);

这样做了之后,A和B访问的时候就不存在覆盖原来的Token的问题.也就解决了上述问题

  1. 如何防止用户重复登录, 思路也是很简单的,就是在application域中存储每一个登录的用户名,以map的形式存储,key就是用户名,用户登录之前判断下map中有没有改用户,就好了

你可能感兴趣的:(Servlet,表单重复提交,ajax)