关于表单的提交相信作为一个后端开发接触过不少,本文将介绍如何解决表单重复提交的问题。
我们通过一个 jsp 页面提交表单到 servlet 进行处理。项目结构如下:
首先看 JSP 页面:from01.jsp
< %@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"% >
< %
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ path;
% >
< !DOCTYPE html >
< head >
< title >Title< /title >
< /head >
< body >
< form action="< %=basePath% >/toServlet01" method="post" >
用户名:< input type="text" name="userName" >
< input type="submit" value="提交" id="submit" >
< /form >
< /body >
< /html >
接着我们看 servlet 操作:
package com.ys.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Create by YSOcean
*/
@WebServlet("/toServlet01")
public class FormServlet01 extends HttpServlet{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("userName");
try {
//模拟网络延时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("提交表单");
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().print("提交成功!!!");
}
}
我们将该项目部署到 tomcat 服务器,然后启动服务器,在浏览器中输入相应地址,点击表单中的提交按钮,后台正常情况下应该打印出提交表单的字样,然后前台页面输出提交成功。
上面我们演示的是正常点击提交的情况,但是实际上用户可能进行多次提交的操作。
①、多次点击提交按钮
这是最明显的一种情况,可能由于我们点击一次按钮后,系统后台对提交操作进行处理有一定的延时,于是页面停在表单提交页面。而当前用户不知道,以为没有提交表单,于是又进行按钮点击,造成表单多次提交。
②、用户提交表单成功之后不断点击浏览器【刷新】按钮
③、提交表单成功后,点击浏览器【回退】箭头,回到表单提交页面,然后重新点击提交按钮
①、onsubmit() 方法
在表单中增加onsubmit() 方法,该方法在表单提交时触发,返回false时,表单就不会被提交。针对用户多次点击按钮提交的问题,我们在前端控制表单提交一次之后,将 onsubmit() 方法返回值改为false,那么第二次点击提交按钮,表单将不能进行提交。
< %@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"% >
< %
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ path;
% >
< !DOCTYPE html >
< head >
< title >Title< /title >
< /head >
< script type="text/javascript" >
var isFlag = false;
function dosubmit(){
if(!isFlag){
isFlag = true;
return true;
}else{
return false;
}
}
< /script >
< body >
< form action="< %=basePath% >/toServlet01" method="post" onsubmit="return dosubmit()" >
用户名:< input type="text" name="userName" >
< input type="submit" value="提交" id="submit" >
< /form >
< /body >
< /html >
②、表单提交之后,将按钮设置不可点击
function dosubmit(){
//获取表单提交按钮
var btnSubmit = document.getElementById("submit");
//将表单提交按钮设置为不可用,这样就可以避免用户再次点击提交按钮
btnSubmit.disabled= "disabled";
//返回true让表单可以正常提交
return true;
}
存在问题:前面这两种方法只能应对用户多次点击提交按钮的情况,也就是上面的第一种情况。但是对于提交之后多次刷新以及点击回退按钮,再次提交的这两种情况却没有效果。这时候就需要在后端进行解决。
具体做法:
在服务器端生成一个唯一的随机标识号,专业术语称为Token(令牌),同时在当前用户的Session域中保存这个Token。然后将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端,然后在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的标识号。
在下列情况下,服务器程序将拒绝处理用户提交的表单请求:
1、存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。(包括伪造Token)
2、当前用户的Session中不存在Token(令牌)。
3、用户提交的表单数据中没有Token(令牌)。
①、首先通过服务器端的 servlet 跳转到表单提交页面:
package com.ys.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
/**
* Create by YSOcean
*/
@WebServlet("/toForm")
public class ToFromServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String tokenId = UUID.randomUUID().toString();
req.getSession().setAttribute("tokenId",tokenId);
req.getRequestDispatcher("from01.jsp").forward(req,resp);
}
}
②、表单页面增加隐藏域存储tokenId
< %@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" isELIgnored="false"% >
< %
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ path;
% >
< !DOCTYPE html >
< head >
< title >Title< /title >
< /head >
< script type="text/javascript" >
var isFlag = false;
/*function dosubmit(){
if(!isFlag){
isFlag = true;
return true;
}else{
return false;
}
}*/
function dosubmit(){
//获取表单提交按钮
var btnSubmit = document.getElementById("submit");
//将表单提交按钮设置为不可用,这样就可以避免用户再次点击提交按钮
btnSubmit.disabled= "disabled";
//返回true让表单可以正常提交
return true;
}
< /script >
< body >
< form action="< %=basePath% >/toServlet01" method="post" onsubmit="return dosubmit()" >
< input type="hidden" name="tokenId" value="${tokenId}" >
用户名:< input type="text" name="userName" >
< input type="submit" value="提交" id="submit" >
< /form >
< /body >
< /html >
③、提交表单,后端进行是否重复判断
package com.ys.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Create by YSOcean
*/
@WebServlet("/toServlet01")
public class FormServlet01 extends HttpServlet{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
String username = req.getParameter("userName");
Boolean flag = isRepeatSubmit(req);
if(flag){
resp.getWriter().print("请不要重复提交!!!");
return;
}
try {
//模拟网络延时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("提交表单");
resp.getWriter().print("提交成功!!!");
}
private boolean isRepeatSubmit(HttpServletRequest request){
//1、获取存储在request域中的tokenId
String req_tokenId = request.getParameter("tokenId");
//req_tokenId == null 表示表单中没有token,即用户不是通过servlet跳转到该页面或者是重复提交
if(req_tokenId == null) {
return true;
}
//2、获取存储在session域中的tokenId
String session_tokenId = (String) request.getSession().getAttribute("tokenId");
//如果当前session域中的tokenId为null,则表示用户重复提交(每次提交之后会移除该session域中的tokenId)
if(session_tokenId == null){
return true;
}
//3、存储在session域中的tokenId和表单隐藏域保存提交的tokenId不同,则表示用户伪造tokenId或者重复提交
if(!session_tokenId.equals(req_tokenId)){
return true;
}
//移除session域中的tokenId
request.getSession().removeAttribute("tokenId");
return false;
}
}
上面主要是利用一次回话中session域存储的数据是保持不变的,而request域只能保存一次请求的数据。
注意:页面首先要通过 servlet 进行跳转过去,不能直接访问jsp页面。先在 servlet 中生成一个 tokenId,然后将tokenId存入到session域中,在转发到jsp表单页面,在表单页面中,通过隐藏域存放生成的tokenId,然后点击提交按钮,会将隐藏域的tokenId 也一起提交到后端。后端首先判断表单中的tokenId值,以及和session域中的tokenId 值进行对比,表单中的tokenId为null,则说明是直接访问的jsp页面,session域中的tokenId 为null,则说明不是第一次提交,因为第一次提交成功之后会清空session域中的tokenId。都不为null,且两者不相等,则说明可能是伪造的tokenId;不为null,且相等,则说明是第一次提交。
这里要注意销毁session域中的tokenId时机,是在判断完是否重复提交的方法中最后就销毁了,这样可以防止还没销毁session域中的tokenId,客户端的请求又来了。
通过上面前后端的解决表单重复提交的问题,我们看似解决了,其实不然,对于各种分布式项目,为了解决高并发的问题,我们会将前端请求通过 nginx 负载到多个tomcat服务器,如下:
这里会存在这样一个问题:
首先通过 tomcat1 将请求跳转到表单页面,这时候tokenId 是存放在tomcat1 session域中,然后点击提交按钮,nginx 可能会将我们的请求分发到 tomcat2 上,而tomcat2 的session 域中是不存在 tokenId 的,这时候我们提交不了表单。
这也是session共享问题。也就是说我们必须找到一个存放 tokenId 的公共介质,无论是哪个服务器去处理请求,都是从公共介质中获取 tokenId,那么当然不会存在tokenId 不一致的问题。
解决办法:
①、利用数据库同步:也就说将 tokenId 存放在数据库中,每次获取的时候从数据库中查询,这能解决,但是对数据的访问压力增大,不太合适。
②、利用 cookie 同步:因为 cookie 是存在本地客户端的,第一次请求我们将tokenId 存放在cookie中,然后从cookie进行是否重复提交校验,这也能解决问题。但是cookie 存在安全性问题,而且每次http请求都要带上参数也增加了带宽消耗。
③、利用 Redis 同步:这是最好的一种办法,Redis是一个高性能缓存框架,我们将 tokenId 存放在Redis中,获取也从Redis中获取,而且Redis性能极佳。
全部0条评论
快来发表一下你的评论吧 !