表单重复提交的常见应用场景
1、在网络延迟的情况下让用户又是加你点击多次submit按钮导致
2、表单提交后用户点击刷新按钮导致表单重复提交
3、用户表单提交后,点击浏览器后退按钮退回表单页面后进行再次提交
很多情况下,重复提交的数据,都不是我们想要的,如订单的提交,申请退款的提交等,那么如何做到防重提交呢?
下面介绍有4种方法,重点最后一种:
1.给数据库所需的字段添加上唯一性约束
此方法最有效的防止了数据重复提交,但是前台还是会出现重复提交的情况,后台回报错.
2.前端使用js在点击按钮提交后设置disable,后或者js设置一个属性,提交前为true,提交后为false
客户端禁用js,这种方法将无效
3.使用Post/Redirect/Get
Post/Redirect/Get简称PRG,是一种可以防止表单数据重复提交的一种Web设计模式,像用户刷新提交响应页面等比较典型的重复提交表单数据的问题可以使用PRG模式来避免。例如:当用户提交成功之后,执行客户端重定向,跳转到提交成功页面。
注意:PRG设计模式并不适用所有的重复提交情况,比如:
1)由于服务器响应缓慢,用户刷新提交POST请求造成的重复提交。
2)用户点击后退按钮,返回到数据提交界面,导致的数据重复提交。
3)用户多次点击提交按钮,导致的数据重复提交。
4)用户恶意避开客户端预防多次提交手段,进行重复数据提交。
4.使用session和注解设置令牌
所谓令牌其实就是一种标识,标识当前提交状态,比如以前古代打战时,皇帝发布命令时会给个虎符给手下的人携带到将军那边传达,而将军手上也有皇帝给的另一半虎符,将军一对比,果然能凑成一对,就根据传达的命令去打战,如果来人没有携带虎符,将军是肯定把来人砍头的.
那么这个令牌怎么设置,下图就是写这个令牌token的一个思路:
1.先写一个简单的注解类
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
自定义一个Token注解,用于标识需要防重提交的方法
@author ranger
*
*/
@Target(value=ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TokenForm {
//用于标记需要防重提方法的 ,创建Token的属性
boolean create() default false;
//用于标记需要防重提方法的,删除Token的属性
boolean remove() default false;
}
2.在跳转到增加页面前,创建token
/**
* 跳转到增加页面
* 请求路径:${pageContext.request.contextPath}/admin/toAdminAdd
* @return
*/
@RequestMapping(value="/toAdminAdd")
@TokenForm(create=true)
public String toAdminAdd(HttpServletRequest request) {
return "manager/adminAdd";
}
3.添加数据后删除token
/**
* 增加管理员
* 请求路径:${pageContext.request.contextPath }/admin/addAdmin
* @param admin
* @param request
* @return
*/
@RequestMapping(value="/addAdmin")
@TokenForm(remove=true)
public String addAdmin(@RequestParam Map admin,HttpServletRequest request) {
try {
//将密码Md5编码后在插入
admin.put("admin_pwd",Md5Utils.md5((String)admin.get("admin_pwd")) );
LOGGER.debug("-增加管理员-"+admin);
adminService.addAdmin(admin);
request.setAttribute("admin_add_msg", "增加管理员成功");
} catch (Exception e) {
e.printStackTrace();
request.setAttribute("admin_add_msg", "增加管理员失败");
}
return "manager/adminAdd";
}
4.创建一个拦截器,拦截发送路径前的token信息
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import cn.gzsxt.annotation.TokenForm;
public class TokenInterceptor implements HandlerInterceptor {
private static final Logger LOGGER = LogManager.getLogger(TokenInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//第一步:获得调用处理方法的注解
HandlerMethod hm=(HandlerMethod) handler;
TokenForm tokenForm = hm.getMethodAnnotation(TokenForm.class);
//第二步:判断是否有Token注解
if (tokenForm!=null) {
HttpSession session = request.getSession();
if (tokenForm.create()==true) {
session.setAttribute("token", UUID.randomUUID().toString());
LOGGER.debug("打印出来的token:"+session.getAttribute("token"));
}
if (tokenForm.remove()==true) {
//判断表单的Token与服务端的Token是否相同
String formToken = request.getParameter("token");
Object sessionToken = session.getAttribute("token");
//传递过来的Token与服务端的Token相同,允许操作,并且删除session的Token
if (formToken.equals(sessionToken)){
session.removeAttribute("token");
}else{
//跳转到指定的路径
String invoke = request.getParameter("token.invoke");
response.sendRedirect(request.getContextPath()+invoke);
return false;
}
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// TODO Auto-generated method stub
}
5.前端指定提交的token和重复提交后可跳转的地址token.invoke