重复提交表单的解决方案--java语言描述
本文的内容参考了文章有
JavaWeb学习总结(十三)——使用Session防止表单重复提交
感谢你阅读本篇文章
初写文章总结作为技术总结,感谢你的阅读。
1 场景是什么?
1.0 懒人专用之完整实例代码下载 与下文的主要结论说明
完整代码下载(包含四个解决方案的实现demo)
主要结论,推荐使用session token机制防止表单提交
1.1.必要的环境搭建
以下是搭建流程:
IDEA新建一个 maven -webapp项目
在src/main里新建一个java目录
src/main/java

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<name>javanote</name>
<groupId>com.mynote</groupId>
<artifactId>javanote</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- JSP -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<!-- JSTL -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>
<!--slf4j-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.7</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.33</version>
<scope>runtime</scope>
</dependency>
<!--Apache commons lang-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.3.2</version>
</dependency>
<!--Apache commons collections-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
<!--Junit-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.0.1</version>
</dependency>
<!--Junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-dbutils/commons-dbutils -->
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Compile -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
<!-- Test -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<!-- Tomcat -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/${project.artifactId}</path>
</configuration>
</plugin>
</plugins>
</build>
</project>
servlet
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<!-- 基本配置 -->
<servlet>
<servlet-name>formServlet</servlet-name>
<servlet-class>DoFormServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>formServlet</servlet-name>
<url-pattern>/servlet/DoFormServlet</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
</welcome-file-list>
</web-app>
index.jsp
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML>
<html>
<head>
<title>Form表单</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/servlet/DoFormServlet" method="post">
用户名:<input type="text" name="username">
<input type="submit" value="提交" id="submit">
</form>
</body>
</html>
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class DoFormServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, 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();
}
//request.getRequestDispatcher("/success.jsp").forward(request, response);
System.out.println("insert into database:"+userName);
//response.sendRedirect("/javanote/success.jsp");
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
试着运行一下看看有没有部署错误
edit->configurations-> 选择 add +maven
Command line上输入
tomcat7:run

输入表单数据,出现

1.2 场景复现
1.2.1 单机场景下
- 场景一:在网络延迟的情况下让用户有时间点击多次submit按钮导致表单重复提交
原理:因为网络缓慢,用户当前页面没有跳转,导致用户多点击了一次按钮,触发了同一个GET/POST请求
- 场景二:表单提交后用户点击【刷新】按钮导致表单重复提交
原理:刷新有同样的效果也是一样,form提交表单后,如果没有处理完整/处理中,当前的状态是在请求form表单的action,所以再刷新会重复该请求。
- 场景三:用户提交表单后,点击浏览器的【后退】按钮回退到表单页面后进行再次提交
这个是换了个逆向的思路,得到了结果,后退,然后再提交
注意:这三个场景的原理都是:同样的URL 不停的点击,触发请求。
1.2 分布式场景下(本文先不讨论)
-
场景和单机一样,但是后端的实现方案不一样(因为单机场景下机器是一台,而分布式环境下机器是多台的。),我们稍后会讨论一下两个的实现方案的区别。
在分布式环境下用redis 代替session
2.解决方案
思路
我们现在已经知道我们要解决的问题是如何防止用户提交相同的URL与会产生相同URL的场景。
那么我们可以有什么解决方案呢?我个人认为要尽可能的拿到所有的解决方案,需要从运行流程涉及到多少组件的输入输出,就可能有多少种解决方案。

我们可以再前端get和post请求到服务器之前,就防止表单提交,常见的有
方案选择原则
前后端需要结合。按场景和开发工作量进行选择。
1.javascript防止表单提交(不推荐)
<form method="post" action="${pageContext.request.contextPath}/servlet/doFormServlet" onsubmit="return dosubmit()" method="post">
javascript 防止表单提交:<input type="text" name="username">
<input type="submit" value="提交" >
<script>
var isCommitted = false;//表单是否已经提交标识,默认为false
function dosubmit(){
if(isCommitted==false){
isCommitted = true;//提交表单后,将表单是否已经提交标识设置为true
return true;//返回true让表单正常提交
}else{
return false;//返回false那么表单将不提交
}
}
</script>
</form>
方案解析与优缺点分析
前提条件:无
代码解析
知识点1:onsubmit="return dosubmit()" ; 加了return 就说明会带着dosubmit()的返回值 所以结果 要不就是 onsubmit="true" 或者onsubmit="false"
知识点2:而return false 是阻止浏览器的默认行为,也就是阻止提交表单
知识点3:而因为 isCommitted是全局变量,所以只会赋值一次。
知识点4:我们在提交表单时会触发onsubmit 事件
我们讨论第一次提交表单的时候,会触发dosubmit()方法,因为 isCommited==false 会触发isCommitted=true方法,然而第二次进来的提交表单的方法也会触发dosubmit()的方法,然而此时 isCommited已经被设置为true了,所以无法第二次提交表达。达到表单只能提交一次的效果。
然而这个问题的缺点也很明显,只要重新刷新一遍或者后退提交也或者直接postman请求能提交表单,便能第二次提交表单,原因是因为isCommited被重新复制为false,所以这个方法不太推荐。
优点:
- 简单,方便
缺点:
- 如果客户端禁用JS,这种方法就会失效
- 用户通过刷新页面方式,或者后退提交或使用postman等工具绕过前段页面仍能重复提交表单
- 后退提交依然生效
2.Post-Redirect-Get (PRG模式)(不推荐)
前提条件:
表单提交后不会跳转的场景。在表单提交后,你去执行一个用户重定向,跳转到提交成功的信息。这能避免用户按F5导致的重复提交。简言之就是不要让表单封装的参数url停留在当前页面中,因为如果停留在当前页面,按F5会产生一样的请求。
sendredirect 和forward 的区别:
扩展 sendredirect(重定向)方法和forward(跳转)方法 的效果都是从一个请求跳到另一个请求,那么就有几个显著的特点
第一个那就是 浏览器的url显示
第二个那就是 请求的参数能不能带过去的问题
可以从英文直接指导 sendredirect 重定向,重新定位,重定向意味着url也会跟着改变,重定向也意味着 是不带参数过去的,foward是带参数过去的,但是不带url的改变。因为只是跳转
更深层次的原理请参考
Servlet之forward、sendRedirect、 include区别与使用 带例子。
sendRedirect方法
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException,IOException{
  response.setContentType("text/html; charset=utf-8");
  response.sendRedirect("/success.jsp");
}
forward方法
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException,IOException{
  response.setContentType("text/html; charset=utf-8");
  ServletContext sc = getServletContext();
  sc.getRequestDispatcher("/success.jsp").forward(request, response);
}
方案解析与优缺点分析
优点:通过后端控制跳转,防止了F5的刷新直接请求
缺点:无法防止后退更新。
3.从数据库方面想解决方案(不推荐)
测试sql
create database test;
use test;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(64) NOT NULL AUTO_INCREMENT,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_c
ALTER TABLE `test`.`user`
ADD UNIQUE INDEX(`username`) ;
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<!-- 基本配置 -->
<servlet>
<servlet-name>formServlet</servlet-name>
<servlet-class>DoFormServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>formServlet</servlet-name>
<url-pattern>/servlet/doFormServlet</url-pattern>
</servlet-mapping>
<!-- 基本配置 -->
<servlet>
<servlet-name>unieKeyTest</servlet-name>
<servlet-class>UnieKeyTestServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>unieKeyTest</servlet-name>
<url-pattern>/servlet/unieKeyTest</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
</welcome-file-list>
</web-app>
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML>
<html>
<head>
<title>Form表单</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/servlet/doFormServlet" method="post">
用户名:<input type="text" name="username">
<input type="submit" value="提交" id="submit">
</form>
<form method="post" action="${pageContext.request.contextPath}/servlet/doFormServlet" onsubmit="return dosubmit()" method="post">
javascript 防止表单提交:<input type="text" name="username">
<input type="submit" value="提交" >
<script>
var isCommitted = false;//表单是否已经提交标识,默认为false
function dosubmit(){
if(isCommitted==false){
isCommitted = true;//提交表单后,将表单是否已经提交标识设置为true
return true;//返回true让表单正常提交
}else{
return false;//返回false那么表单将不提交
}
}
</script>
</form>
<form method="post" action="${pageContext.request.contextPath}/servlet/unieKeyTest">
唯一索引表单提交测试:<input type="text" name="username"/>
<input type="submit"/>
</form>
</body>
</html>
service及时捕捉插入数据异常:
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class UnieKeyTestServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/test";
request.setCharacterEncoding("UTF-8");
conn = DriverManager.getConnection(url,"root","root");
String sql = "insert into user(username) values(?);";
PreparedStatement ps = conn.prepareStatement(sql);
String username=request.getParameter("username");
try {
//让当前的线程睡眠3秒钟,模拟网络延迟而导致表单重复提交的现象
Thread.sleep(3*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ps.setString(1,username);
try{
ps.executeUpdate();
}catch(Exception e){
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
方案解析与优缺点分析
唯一索引是索引的一种,索引在增加和更新或者删除时,是比较慢的。否则为什么不在一开始建表的时候全部都加上索引呢?索引让每个表加上唯一索引 不是一个很好的解决方案,增加了增删更新的开销,除非是比较大的表。(数据量过百万的表)
这个方案的优点是:只要确定了位移索引的字段,那么表里的数据肯定不会重复。简单,快捷。
缺点:同一个主键会抛出DuplicateKeyException 异常,要注意捕捉,数据量没百万 建立索引,增删更新会浪费较多性能。
4.session token机制(推荐)
对于上述问题,我们发现了几个无法解决的问题
- 表单F5刷新
- 后退提交
- 网络延迟下提交
他们都产生了一个现象那就是相同的URL。那么有没有办法每一个URL都不一样呢?对,那就是加一个随机token。
具体的做法:在服务器端生成一个唯一的随机标识号,专业术语称为Token(令牌),同时在当前用户的Session域中保存这个Token。然后将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端,然后在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的标识号。 在下列情况下,服务器程序将拒绝处理用户提交的表单请求:
- 存储Session域中的Token(令牌)与表单提交的Token(令牌)不同.
- 当前用户的Session中不存在Token(令牌)
- 用户提交的表单数据中没有(令牌)。
web.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<!-- 基本配置 -->
<servlet>
<servlet-name>formServlet</servlet-name>
<servlet-class>DoFormServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>formServlet</servlet-name>
<url-pattern>/servlet/doFormServlet</url-pattern>
</servlet-mapping>
<!-- 基本配置 -->
<servlet>
<servlet-name>generateTokenServlet</servlet-name>
<servlet-class>GenerateTokenServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>generateTokenServlet</servlet-name>
<url-pattern>/servlet/generateTokenServlet</url-pattern>
</servlet-mapping>
<!-- 基本配置 -->
<servlet>
<servlet-name>testF5Flash</servlet-name>
<servlet-class>TestF5Flash</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>testF5Flash</servlet-name>
<url-pattern>/servlet/TestF5Flash</url-pattern>
</servlet-mapping>
<!-- 基本配置 -->
<servlet>
<servlet-name>unieKeyTest</servlet-name>
<servlet-class>UnieKeyTestServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>unieKeyTest</servlet-name>
<url-pattern>/servlet/unieKeyTest</url-pattern>
</servlet-mapping>
<!-- 基本配置 -->
<servlet>
<servlet-name>tokenSolveServlet</servlet-name>
<servlet-class>TokenSolveServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>tokenSolveServlet</servlet-name>
<url-pattern>/servlet/tokenSolveServlet</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
</welcome-file-list>
</web-app>
GenerateTokenServlet.java
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class GenerateTokenServlet extends HttpServlet {
private static final long serialVersionUID = -884689940866074733L;
@Override
public void doGet(HttpServletRequest request, 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("/index.jsp").forward(request, response);//跳转到form.jsp页面
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
TokenProcessor生成token
通过生成时间戳+一个 小于999999999的随机数 生成token
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import sun.misc.BASE64Encoder;
public class TokenProccessor {
/*
*单例设计模式(保证类的对象在内存中只有一个)
*1、把类的构造函数私有
*2、自己创建一个类的对象
*3、对外提供一个公共的方法,返回类的对象
*/
private TokenProccessor(){}
private static final TokenProccessor instance = new TokenProccessor();
/**
* 返回类的对象
* @return
*/
public static TokenProccessor getInstance(){
return instance;
}
/**
* 生成Token
* Token:Nv6RRuGEVvmGjB+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 = new BASE64Encoder();
return encoder.encode(md5);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
jsp页面
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML>
<html>
<head>
<title>Form表单</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/servlet/doFormServlet" method="post">
用户名:<input type="text" name="username">
<input type="submit" value="提交" id="submit">
</form>
<form method="post" action="${pageContext.request.contextPath}/servlet/doFormServlet" onsubmit="return dosubmit()" method="post">
javascript 防止表单提交:<input type="text" name="username">
<input type="submit" value="提交" >
<script>
var isCommitted = false;//表单是否已经提交标识,默认为false
function dosubmit(){
if(isCommitted==false){
isCommitted = true;//提交表单后,将表单是否已经提交标识设置为true
return true;//返回true让表单正常提交
}else{
return false;//返回false那么表单将不提交
}
}
</script>
</form>
<form action="${pageContext.request.contextPath}/servlet/tokenSolveServlet" method="post">
<input type="hidden" name="token" value="${token}"/>
Token表单重复提交测试 用户名:<input type="text" name="username">
<input type="submit" value="提交">
</form>
</body>
</html>
java 文件
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class TokenSolveServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, 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 boolean isRepeatSubmit(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;
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
方案解析与优缺点分析
优点:几乎解决所有问题。
缺点:几乎没有确定。就是生成token比较麻烦