0%

表单重复提交

Java Web 表单重复提交问题:原因分析与解决方案

表单重复提交是 Web 开发中常见的问题,可能导致数据重复入库、订单重复创建等严重后果。本文将详细分析重复提交的场景、成因,并提供基于 Session 的解决方案,确保表单数据仅被处理一次。

表单重复提交的常见场景

1. 场景一:提交后刷新响应页面

  • 操作流程:用户提交表单 → 服务器通过 request.getRequestDispatcher().forward() 转发到结果页 → 用户刷新结果页。
  • 原因:刷新时浏览器会重新发送最后一次请求(即表单提交请求),而地址栏仍显示 Servlet 路径,导致重复提交。

2. 场景二:快速重复点击提交按钮

  • 操作流程:用户在短时间内多次点击 “提交” 按钮。
  • 原因:服务器尚未处理完第一次请求,第二次请求已到达,导致重复处理。

3. 场景三:后退后再次提交

  • 操作流程:用户提交表单 → 点击浏览器 “后退” 按钮 → 再次点击 “提交” 按钮。
  • 原因:后退后表单数据仍保存在浏览器中,再次提交会重新发送相同请求。

注意:不属于重复提交的情况

点击 “后退”→ 刷新页面 → 再次提交,不属于重复提交。因为刷新页面会重新加载表单页,此时提交的是新的请求(原表单数据可能已清空或重新生成)。

解决方案:基于 Session 的令牌验证机制

核心思路是生成唯一令牌(Token),通过 Session 跟踪令牌状态,确保每个表单请求仅被处理一次。

实现步骤:

步骤一:表单页面生成令牌并存储到 Session

在表单页面(如 JSP)生成一个唯一令牌(可使用 UUID),将令牌存入 Session,并在表单中隐藏字段携带该令牌。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<%@ page import="java.util.UUID" %>
<%
// 生成唯一令牌
String token = UUID.randomUUID().toString();
// 存储到 Session
session.setAttribute("formToken", token);
%>

<form action="/submit" method="post">
<!-- 隐藏字段携带令牌 -->
<input type="hidden" name="token" value="<%= token %>">
<input type="text" name="username">
<button type="submit">提交</button>
</form>
步骤二:Servlet 验证令牌并处理请求

在 Servlet 中,通过以下逻辑验证令牌:

  • 从请求参数中获取表单提交的令牌;
  • 从 Session 中获取存储的令牌;
  • 对比两个令牌:若一致,处理请求并删除 Session 中的令牌;若不一致或令牌不存在,视为重复提交。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@WebServlet("/submit")
public class SubmitServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");

// 1. 获取表单提交的令牌
String clientToken = request.getParameter("token");
// 2. 获取 Session 中存储的令牌
String serverToken = (String) request.getSession().getAttribute("formToken");

// 3. 验证令牌
if (clientToken == null || serverToken == null || !clientToken.equals(serverToken)) {
// 令牌无效或已被使用(重复提交)
response.getWriter().write("表单已提交,请勿重复提交!");
return;
}

// 4. 令牌验证通过,处理业务逻辑
String username = request.getParameter("username");
System.out.println("处理提交:" + username); // 模拟数据库操作

// 5. 移除 Session 中的令牌(确保只能使用一次)
request.getSession().removeAttribute("formToken");

// 6. 重定向到结果页(避免刷新重复提交)
response.sendRedirect(request.getContextPath() + "/success.jsp");
}
}
步骤三:使用重定向避免刷新重复提交

处理完请求后,通过 response.sendRedirect() 重定向到结果页,而非请求转发(forward)。

  • 重定向会改变地址栏 URL,刷新时只会重新请求结果页,而非表单提交请求;
  • 转发会保留原 Servlet 路径,刷新时会重复提交表单。
1
2
3
<!-- success.jsp -->
<h1>提交成功!</h1>
<a href="/form.jsp">返回表单页</a>

增强方案:防止快速点击与前端配合

1. 前端禁用提交按钮(辅助手段)

在用户点击提交后立即禁用按钮,防止短时间内重复点击:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<form action="/submit" method="post" onsubmit="return disableSubmit()">
<input type="hidden" name="token" value="<%= token %>">
<input type="text" name="username">
<button type="submit" id="submitBtn">提交</button>
</form>

<script>
function disableSubmit() {
// 禁用提交按钮
document.getElementById("submitBtn").disabled = true;
// 允许表单提交
return true;
}
</script>

注意:前端措施仅为辅助,必须配合后端令牌验证(防止绕过前端限制的恶意提交)。

2. 令牌过期机制(可选)

为令牌设置过期时间,避免长期有效的令牌被滥用:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 生成令牌时记录时间戳
String token = UUID.randomUUID().toString();
long expireTime = System.currentTimeMillis() + 30 * 60 * 1000; // 30分钟过期
request.getSession().setAttribute("formToken", token);
request.getSession().setAttribute("tokenExpireTime", expireTime);

// 验证时检查是否过期
long currentTime = System.currentTimeMillis();
long expireTime = (long) request.getSession().getAttribute("tokenExpireTime");
if (currentTime > expireTime) {
response.getWriter().write("表单已过期,请重新加载!");
return;
}

方案原理总结

基于 Session 的令牌验证机制通过 “一次令牌,一次提交” 的原则避免重复提交:

  1. 生成令牌:表单加载时生成唯一令牌,存入 Session 并随表单发送;
  2. 验证令牌:服务器对比请求令牌与 Session 令牌,确保一致性;
  3. 失效令牌:处理请求后立即删除 Session 中的令牌,使其无法再次使用;
  4. 重定向结果:避免刷新页面导致的重复提交。

该方案能有效解决所有场景的重复提交问题,是 Web 开发中的标准实践。

注意事项

  1. Session 依赖:令牌存储依赖 Session,需确保用户会话有效(如未超时、未关闭浏览器)。
  2. 分布式环境:集群部署时,需使用分布式 Session(如 Redis 共享 Session),避免令牌存储在单个节点导致验证失败。
  3. 安全性:令牌应足够随机(如 UUID),防止被猜测或伪造。

欢迎关注我的其它发布渠道