表单防重复提交

防止表单重复提交

介绍了使用 redirect 技术防止表单提交,但是 redirect 解决不了后退到表单页面时重复提交表单,为了解决这个问题,加入了 token 的机制。如果每个 form 相关的处理方法中都写一遍 token 的生成和校验代码,在实际项目中是不太能接受的,接下来介绍了使用拦截器的方式生成和校验 token。

1. 常规防止表单重复提交流程:

  1. GET 访问表单页面
  2. 填写表单
  3. POST 提交表单
  4. Server 端处理表单数据,例如把数据写入数据库
  5. 重定向到另一个页面,防止用户刷新页面重复提交表单


result.htm

Result: ${result!}

user-form.htm

<!DOCTYPE html>
<html>
<head>
    <title>Update User</title>
</head>
<body>
<form action="/user-form" method="post">
    Username: <input type="text" name="username"><br>
    Password: <input type="text" name="password"><br>
    <button type="submit">Update User</button>
</form>
</body>
</html>

ParameterController

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
public class ParameterController {
    // 显示表单
    @RequestMapping(value = "/user-form", method= RequestMethod.GET)
    public String showUserForm() {
        return "user-form.htm";
    }

    // 更新 User,把操作结果保存到 redirectAttributes,
    // redirect 到 result 页面显示操作结果
    @RequestMapping(value = "/user-form", method= RequestMethod.POST)
    public String handleUserForm(@RequestParam String username,
                                 @RequestParam String password,
                                 final RedirectAttributes redirectAttributes) {
        // Update user in database...
        System.out.println("Username: " + username + ", Password: " + password);

        // 操作结果显示给用户
        redirectAttributes.addFlashAttribute("result", "The user is already successfully updated");

        return "redirect:/result"; // URI instead of viewName
    }

    // 显示表单处理结果
    @RequestMapping("/result")
    public String result() {
        return "result.htm";
    }
}

测试一

  1. 访问 http://localhost/user-form
  2. 点击 Update User,表单成功提交后被重定向到 result 页面
  3. 刷新 result 页面,表单没有被重复提交,实现了防止表单重复提交的功能

2. 使用 token 进一步加强防止表单重复提交

但是,如果在浏览器里点击后退按钮后退到表单页面,点击 Update User,表单被再次提交了。可以使用 token 防止后退的情况下重复提交表单,访问表单页面的时候生成一个 token 在 form 里并且在 Server 端存储这个 token,提交表单的时候先检查 Server 端有没有这个 token,如果有则说明是第一次提交表单,然后把 token 从 Server 端删除,处理表单,redirect 到 result 页面,如果 Server 端没有这个 token,则说明是重复提交的表单,不处理表单的提交。


result.htm

Result: ${result!}

user-form.htm

在 form 里增加一个 input 域存放 token.

<!DOCTYPE html>
<html>
<head>
    <title>Update User</title>
</head>
<body>
<form action="/user-form" method="post">
    <input type="input" name="token" value="${token!}"><br>
    Username: <input type="text" name="username"><br>
    Password: <input type="text" name="password"><br>
    <button type="submit">Update User</button>
</form>
</body>
</html>

ParameterController

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.servlet.http.HttpSession;
import java.util.UUID;

@Controller
public class ParameterController {
    // 显示表单
    @RequestMapping(value = "/user-form", method= RequestMethod.GET)
    public String showUserForm(ModelMap model, HttpSession session) {
        String token = UUID.randomUUID().toString().toUpperCase().replaceAll("-", "");

        model.addAttribute("token", token);
        session.setAttribute(token, token);

        return "user-form.htm";
    }

    // 更新 User,把操作结果保存到 redirectAttributes,
    // redirect 到 result 页面显示操作结果
    @RequestMapping(value = "/user-form", method= RequestMethod.POST)
    public String handleUserForm(@RequestParam String username,
                                 @RequestParam String password,
                                 @RequestParam String token,
                                 HttpSession session,
                                 RedirectAttributes redirectAttributes) {
        // 处理表单前,查看 token 是否有效
        if (token == null || token.isEmpty() || !token.equals(session.getAttribute(token))) {
            throw new RuntimeException("重复提交表单");
        }

        // 正常提交表单,删除 token
        session.removeAttribute(token);

        // Update user in database...
        System.out.println("Username: " + username + ", Password: " + password);

        // 操作结果显示给用户
        redirectAttributes.addFlashAttribute("result", "The user is already successfully updated");

        return "redirect:result";
    }

    // 显示表单处理结果
    @RequestMapping("/result")
    public String result() {
        return "result.htm";
    }
}

测试二

  1. 访问 http://localhost/user-form
  2. 点击 Update User,表单成功提交后被重定向到 result 页面
  3. 刷新 result 页面,表单没有被重复提交,实现了防止表单重复提交的功能
  4. 点击后退按钮回到表单页面,点击 Update User,因为 token 不存在了,程序抛出异常,防止了表单的重复提交(显示异常页面不是最好的办法,更友好的做法是显示一个表单重复提交提示页面)

3. 使用 SpringMVC 拦截器生成和验证 token

思考一下,为了给 user-form 增加 token,在处理 user-form 的方法里新加了很多代码,如果有 10 个 form, 100 form 都要使用 token 的机制呢?难道要去每个 form 处理的方法里都加上上面的那么多代码吗?上面 token 使用的是 UUID,如果要改成 static 类型的整数,每次生成时都加 1 呢? token 存储在 session 里,项目进行到一定的时候要决定存储在第三方缓存如 Redis 里呢?每次需求的变更都要修改所有 form 的处理方法? 工作量也太大了,谁遇到这样的问题都会抓狂,难怪招聘里着重强调:不许打项目经理!

幸好 SpringMVC 提供了拦截器的机制,能够很简单的给 form 增加 token 的机制
  • 当访问 user-form 页面时,在拦截器的 postHandle() 里生成 token 存放在 ModelAndView 和 session 里。
  • 提交表单时,在拦截器的 preHandle() 里校验 token,如果 token 无效则禁止表单的提交。
  • 需要增加 token 机制的 form 在拦截器的配置里加上 form 的 URI。
  • 如果 form 不需要 token 机制,从拦截器的配置里把它的 URI 删除即可。
  • 不需要修改 Controller 中的代码。

什么是 token? 简单的说就是一次操作的标识,可以是数字,字符串,甚至对象等,只要能把不同的表单提交区别开来就可以了。申请表单的时候生成一个 token,表单提交后删除 token。


result.htm

Result: ${result!}

user-form.htm

在 form 里增加一个 input 域存放 token.

<!DOCTYPE html>
<html>
<head>
    <title>Update User</title>
</head>
<body>
<form action="/user-form" method="post">
    <input type="input" name="token" value="${token!}"><br>
    Username: <input type="text" name="username"><br>
    Password: <input type="text" name="password"><br>
    <button type="submit">Update User</button>
</form>
</body>
</html>

ParameterController

和开始的 Controller 代码一样,没有 token 的相关代码。

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
public class ParameterController {
    // 显示表单
    @RequestMapping(value = "/user-form", method= RequestMethod.GET)
    public String showUserForm() {
        return "user-form.htm";
    }

    // 更新 User,把操作结果保存到 redirectAttributes,
    // redirect 到 result 页面显示操作结果
    @RequestMapping(value = "/user-form", method= RequestMethod.POST)
    public String handleUserForm(@RequestParam String username,
                                 @RequestParam String password,
                                 final RedirectAttributes redirectAttributes) {
        // Update user in database...
        System.out.println("Username: " + username + ", Password: " + password);

        // 操作结果显示给用户
        redirectAttributes.addFlashAttribute("result", "The user is already successfully updated");

        return "redirect:result";
    }

    // 显示表单处理结果
    @RequestMapping("/result")
    public String result() {
        return "result.htm";
    }
}

TokenValidator

拦截器 TokenValidator 用于生成和校验 token。

package interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

public class TokenValidator implements HandlerInterceptor {
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        // POST, PUT, DELETE 请求都有可能是表单提交
        if (!"GET".equalsIgnoreCase(request.getMethod())) {
            String clientToken = request.getParameter("token");
            String serverToken = (String) request.getSession().getAttribute(clientToken);

            if (clientToken == null || clientToken.isEmpty() || !clientToken.equals(serverToken)) {
                throw new RuntimeException("重复提交表单");
            }

            // 正常提交表单,删除 token
            request.getSession().removeAttribute(clientToken);
        }

        return true;
    }

    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {
        // GET 请求访问表单页面
        if (!"GET".equalsIgnoreCase(request.getMethod())) {
            return;
        }

        // 生成 token 存储到 session 里,并且保存到 form 的 input 域
        String token = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();

        modelAndView.addObject("token", token);
        request.getSession().setAttribute(token, token);
    }

    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {

    }
}

spring-mvc.xml 里配置拦截器

<beans>
    ...
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/user-form"/> <!--需要增加 token 校验的 form 的 URI-->
            <bean class="interceptor.TokenValidator"></bean>
        </mvc:interceptor>
    </mvc:interceptors>
</beans>

测试三

  1. 访问 http://localhost/user-form
  2. 点击 Update User,表单成功提交后被重定向到 result 页面
  3. 刷新 result 页面,表单没有被重复提交,实现了防止表单重复提交的功能
  4. 点击后退按钮回到表单页面,点击 Update User,因为 token 不存在了,程序抛出异常,防止了表单的重复提交(显示异常页面不是最好的办法,更友好的做法是显示一个表单重复提交提示页面)
通过 SpringMVC 拦截器增加 token 的机制,
  • 想改变 token 生成策略? 修改 TokenValidator
  • 想改变 token 的存储策略? 修改 TokenValidator
  • 想给 form 增加 token 校验? 修改 spring-mvc.xml 拦截器的配置
  • 想把 form 的 token 校验删除? 修改 spring-mvc.xml 拦截器的配置
  • 不需要修改任何 form 处理的方法,泰山崩于前而色不变,风波骤起而泰然处之,项目经理好像也没那么可恨了
提示:

Token 的存储需要考虑过期时间,否则访问 10 万次 user-form 页面,生成 10 万个 token 而不提交表单,token 一直不会被删除,会造成很大的资源浪费。

Token 应该写入到 form 的隐藏域,为了直观,上面我们写入了普通的 input 中:
<input type="hidden" name="token" value="${token!}">

这里使用的是 SpringMVC 的拦截器生成和校验 token,当然也可以使用 Servlet 的 Filter 等技术实现。

重复提交表单时不应该直接把异常显示给用户,可以使用 SpringMVC 的异常处理机制,不同的页面显示不同异常的友好信息

时间: 2024-10-06 02:12:09

表单防重复提交的相关文章

struts2 文件的上传下载 表单的重复提交 自定义拦截器

文件上传中表单的准备 要想使用 HTML 表单上传一个或多个文件 须把 HTML 表单的 enctype 属性设置为 multipart/form-data 须把 HTML 表单的method 属性设置为 post 需添加 <input type=“file”> 字段. Struts 对文件上传的支持 在 Struts 应用程序里, FileUpload 拦截器和 Jakarta Commons FileUpload 组件可以完成文件的上传. 步骤:1. 在 Jsp 页面的文件上传表单里使用

关于防止表单表达重复提交的几个解决方法

表达重复提交的问题,是B/S系统开发中经常容易被忽视,但常常又令程序员头疼的一个问题.根据墨菲定律,如果你不做防止重复提交的机制,那些用户行为往往就会给你带来麻烦,然后就等着产品经理的抱怨吧.下面,我就总结了几条常见的关于B/S系统中防止表单重复提交的几个办法: 1.页面上控制.怕用户点击提交按钮2次?用javascript控制下吧:怕用户后退导致重复提交?那就干脆打开个新页面吧.总之你要设想到用户在页面上的所有可能的操作,把这些容易导致BUG的操作消灭的萌芽中. 2.session控制.如果实

HttpSession解决表单的重复提交

1). 重复提交的情况: ①. 在表单提交到一个 Servlet, 而 Servlet 又通过请求转发的方式响应一个 JSP(HTML) 页面, 此时地址栏还保留着 Serlvet 的那个路径, 在响应页面点击 "刷新" ②. 在响应页面没有到达时重复点击 "提交按钮". ③. 点击 "返回", 再点击 "提交" 2). 不是重复提交的情况: 点击 "返回", "刷新" 原表单页面, 再

如何防止表单的重复提交

表单重复提交是在多用户Web应用中最常见.带来很多麻烦的一个问题.有很多的应用场景都会遇到重复提交问题,比如: (1)点击提交按钮两次. (2)点击刷新按钮. (3)使用浏览器后退按钮重复之前的操作,导致重复提交表单. (4)使用浏览器历史记录重复提交表单. (5)浏览器重复的HTTP请求. (6)用户提交表单时可能因为网速的原因,或者网页被恶意刷新,致使同一条记录重复插入到数据库中,这是一个比较棘手的问题.我们可以从客户端和服务器端一起着手,设法避免同一表单的重复提交. 参考博客: https

表单的重复提交,解决方案

表单的重复提交,解决方案: 第一种情况:在提交表单时,如果网速较差,可能会导致点击提交按钮多次: - 解决方法:点击提交按钮之后,使按钮不可用.通过js完成: <script type="text/javascript"> window.onload = function(){ //获取按钮的对象 var btn = document.getElementById("btn"); //为按钮绑定单击响应函数 btn.onclick = function(

防止表单的重复提交

场景有三种: 1:在网络延迟的情况下让用户有时间点击多次submit导致重复提交 2:表单提交后点击"刷新"按钮导致重复提交 3:提交后,点击浏览器的后退然后再次提交 解决的思路有常用的几种: 1.通过JS,提交表单之后将按钮设置为不可用. <script type="text/javascript">        var isCommitted = false;//表单是否已经提交标识,默认为false        function dosubmi

structs2 防止表单的重复提交token

一.简介 Struts2使用token拦截器来检查表单是否重复提交,采用的是同步令牌的方式. 同步令牌方式:服务器端在处理到达的请求之前,会将请求中包含的令牌值与保存在当前用户会话中的令牌值进行比较,看是否匹配.在处理完该请求后,且在答复发送给客户端之前,将会产生一个新的令牌,该令牌除传给客户端以外,也会将用户会话中保存的旧的令牌进行替换.这样如果用户回退到刚才的提交页面并再次提交的话,客户端传过来的令牌就和服务器端的令牌不一致,从而有效地防止了重复提交的发生. 实现方法: 1)token to

struts2 防止表单的重复提交

防止表单重复提交(拦截器) 1.回顾之前的解决办法: 2.Struts2中的解决办法: 2.1.使用重定向 <result type="redirect">/success.jsp</result> 遗留的问题:防不住后退,再提交. 2.2.使用<s:token/>生成令牌配合token拦截器 1 <%@ page language="java" import="java.util.*" pageEnco

js 防止表单异步重复提交

<form id="formData" method="post" action="${pageContext.request.contextPath }/save"> <input type="button" value="确认提交" class="tj-btn" id="tj"> </form> <script type=