防止表单重复提交

防止表单重复提交的方法

一. 概述

我们在平时项目开发中可能会出现下面这些情况:

  1. 由于用户误操作,多次点击表单提交按钮。
  2. 由于网速等原因造成页面卡顿,用户重复刷新提交页面。
  3. 黑客或恶意用户使用postman等工具重复恶意提交表单(攻击网站)。

这些情况都会导致表单重复提交,造成数据重复。因此,我们需要对这些情况进行一定的预防。

二. 解决方案

1. 通过JavaScript防止表单重复提交

使用js防止表单重复提交的方式,其实就是设置一个全局的标记。在表单提交后,修改该标记的值。从而对重复提交进行预防。但是,并不推荐这种方式,该方法存在存在下列优缺点:

优点:

  • 简单,方便

缺点:

  • 如果客户端禁用JS,这种方法就会失效
  • 用户通过刷新页面方式,或使用postman等工具绕过前段页面仍能重复提交表单

代码示例:

<script type="text/javascript">
//默认提交状态为false
var commitStatus = false;
function submit(){
	if(commitStatus==false){
		//提交表单后,讲提交状态改为true
		commitStatus = true;
		return true;
	}else{
		return false;
	}
}
</script>

我们还可以在修改标记后, 设置提交按钮的禁用:

<script type="text/javascript">
//默认提交状态为false
var commitStatus = false;
function submit(){
	if(commitStatus==false){
		//提交表单后,讲提交状态改为true
		commitStatus = true;
		//设置disabed属性
   		 $("input[type='submit']").attr("disabled",true);
   		// 或者 $("input[type='submit']").attr("disabled","disabled");
   		// 移除disabed属性
   		//$("input[type='submit']").attr("disabled",false);
        //或者 $("input[type='submit']").attr("disabled","");
		return true;
	}else{
		return false;
	}
}
</script>
2. 在数据库里添加唯一约束

在数据库里添加唯一约束或创建唯一索引,防止出现重复数据。这是最有效的防止重复提交数据的方法。但是,通过数据库加唯一键约束能有效避免数据库重复插入相同数据。但无法阻止恶意用户重复提交表单(攻击网站),服务器大量执行sql插入语句,增加服务器和数据库负荷。

数据库加唯一性约束sql:

alter table tableName_xxx add unique key uniq_xxx(field1, field2)

service及时捕捉插入数据异常:

try {
	xxxMapper.insert(user);
} catch (DuplicateKeyException e) {
	logger.error("user already exist");
}
3. Redirect-After-Post模式

在提交后执行页面重定向,这就是所谓的Post-Redirect-Get (PRG)模式。简言之,当用户提交了表单后,你去执行一个客户端的重定向,转到提交成功信息页面。这能避免用户按F5导致的重复提交,而其也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样问题。

(1). 用sendRedirect()函数转向

用户提交表单之后,执行重定向,转到成功信息页面。可避免用户按F5刷新页面和点击浏览器前进或后退导致的重复提交。

public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException,IOException{
  response.setContentType("text/html; charset=utf-8");
  response.sendRedirect("/success.jsp");
}
(2). 用forward()函数转向

除此之外,当用户提交表单,服务器端调用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);
}
4. 利用Session防止表单重复提交

在服务器端生成一个唯一的随机标识号,专业术语称为Token,同时在当前用户的Session域中保存这个Token。然后将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端,然后在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的标识号。

在下列情况下,服务器程序将拒绝处理用户提交的表单请求:

  • 存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。
  • 当前用户的Session中不存在Token(令牌)。
  • 用户提交的表单数据中没有Token(令牌)。

示例代码如下:

  1. 创建FormServlet,用于生成Token(令牌)和跳转到form.jsp页面
public class FormServlet extends HttpServlet {
    private static final long serialVersionUID = -884689940866074733L;
 
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
 
        String token = UUID.randomUUID().toString();//创建令牌
        System.out.println("在FormServlet中生成的token:"+token);
        request.getSession().setAttribute("token", token);  //在服务器使用session保存token(令牌)
        request.getRequestDispatcher("/form.jsp").forward(request, response);//跳转到form.jsp页面
    }
 
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
 
}
  1. 在form.jsp中使用隐藏域来存储Token(令牌)
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>form表单</title>
</head>
 
<body>
    <form action="${pageContext.request.contextPath}/servlet/DoFormServlet" method="post">
        <%--使用隐藏域存储生成的token--%>
        <%--
            <input type="hidden" name="token" value="<%=session.getAttribute("token") %>">
        --%>
        <%--使用EL表达式取出存储在session中的token--%>
        <input type="hidden" name="token" value="${token}"/> 
        用户名:<input type="text" name="username"> 
        <input type="submit" value="提交">
    </form>
</body>
</html>
  1. DoFormServlet处理表单提交
public class DoFormServlet extends HttpServlet {
 
    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;
        }
 
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
 
}
5. 使用AOP自定义切入实现

实现步骤:

  1. 自定义防止重复提交标记(@AvoidRepeatableCommit)。
  2. 对需要防止重复提交的Congtroller里的mapping方法加上该注解。
  3. 新增Aspect切入点,为@AvoidRepeatableCommit加入切入点。
  4. 每次提交表单时,Aspect都会保存当前key到reids(须设置过期时间)。
  5. 重复提交时Aspect会判断当前redis是否有该key,若有则拦截。

(1)自定义注解

import java.lang.annotation.*;
/**
 * 避免重复提交
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AvoidRepeatableCommit {

    /**
     * 指定时间内不可重复提交,单位毫秒
     * @return
     */
    long timeout()  default 30000 ;

}

(2)自定义切入点Aspect

/**
  * 重复提交aop
  */
@Aspect
@Component
public class AvoidRepeatableCommitAspect {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * @param point
     */
    @Around("@annotation(com.xwolf.boot.annotation.AvoidRepeatableCommit)")
    public Object around(ProceedingJoinPoint point) throws Throwable {

        HttpServletRequest request  = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
        String ip = IPUtil.getIP(request);
        //获取注解
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        //目标类、方法
        String className = method.getDeclaringClass().getName();
        String name = method.getName();
        String ipKey = String.format("%s#%s",className,name);
        int hashCode = Math.abs(ipKey.hashCode());
        String key = String.format("%s_%d",ip,hashCode);
        log.info("ipKey={},hashCode={},key={}",ipKey,hashCode,key);
        AvoidRepeatableCommit avoidRepeatableCommit =  method.getAnnotation(AvoidRepeatableCommit.class);
        long timeout = avoidRepeatableCommit.timeout();
        if (timeout < 0){
            timeout = Constants.AVOID_REPEATABLE_TIMEOUT;
        }
        String value = (String) redisTemplate.opsForValue().get(key);
        if (StringUtils.isNotBlank(value)){
            return "请勿重复提交";
        }
        redisTemplate.opsForValue().set(key, UUIDUtil.uuid(),timeout,TimeUnit.MILLISECONDS);
        //执行方法
        Object object = point.proceed();
        return object;
    }

}
6. 使用Cookie对表单状态进行判断

以User(假设有一个user类)举例说明,将用户id和"ok" + id分别放到cookie里面,根据需要设置cookie存活时间,然后放到response里面。在每次提交form表单时,先判断cookie中的name是否是已经提交过的表单名称,如果是就重定向到error页面。

示例代码如下:

Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
    if (String.valueOf(user.getId()).equals(cookie.getValue())) {
        response.sendRedirect("error.jsp");
    } else {
	Cookie cookie2 = new Cookie("ok" + user.getId(), String.valueOf(user.getId()));
	response.addCookie(cookie2);
    }
}

注意:如果客户端禁止了Cookie,该方法将不起任何作用,这点请注意。

7. 拦截器+注解方式
7.1 先自定义防止重复的注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventRepeat {
}
7.2 自定义拦截器
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * 相同url和数据拦截器 为了防止重复提交等操作
 * 继承拦截器适配器
 */
public class SameUrlDataInterceptor extends HandlerInterceptorAdapter {

    /**
     * 覆盖父类的preHandle方法
     * 预处理回调方法,实现处理器的预处理,验证是否为重复提交,第三个参数为响应的处理器,自定义Controller
     * 返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应;
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 1. 判断handler参数是否为HandlerMethod类的实例
        if (handler instanceof HandlerMethod) {

            // 2. 获取方法注解查看方式是否有PreventRepeat注解
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            PreventRepeat annotation = method.getAnnotation(PreventRepeat.class);
            if (annotation != null) {

                // 3. 调用重复数据验证方法
                boolean result = repeatDataValidator(request);
                if(result){
                    return false;
                }
                else{
                    return true;
                }

            }else{
                return true;
            }
        } else {

            // 4. 如果参数不是HandlerMethod类的实例则调用父类的preHandle方法
            return super.preHandle(request, response, handler);
        }
    }

    /**
     * 验证同一个url数据是否相同提交,相同返回true
     * @param httpServletRequest
     * @return
     */
    public boolean repeatDataValidator(HttpServletRequest httpServletRequest) throws Exception{

        try {

            // 1. 将请求参数转换为json字符串 需要在pom内引用jackson-databind
            ObjectMapper objectMapper = new ObjectMapper();
            String params = objectMapper.writeValueAsString(httpServletRequest.getParameterMap());

            // 2. 获取当前请求的url地址 并以url为key 参数为值存在map内
            String url=httpServletRequest.getRequestURI();
            Map<String,String> map=new HashMap(4);
            map.put(url, params);
            String nowUrlParams=map.toString();

            // 3. 获取session中上一次请求存储的url和参数字符串
            Object preUrlParams=httpServletRequest.getSession().getAttribute("oldUrlParams");

            // 4. 如果上一个数据为null,表示还没有访问页面 将当前方位的url和请求参数存储到session中
            if(preUrlParams == null) {
                httpServletRequest.getSession().setAttribute("oldUrlParams", nowUrlParams);
                return false;
            } else {

                // 5. 判断上一次访问的url和参数与本次是否相同 如相同则表示重复数据
                if(preUrlParams.toString().equals(nowUrlParams))
                {
                    return true;
                }
                else
                {
                    httpServletRequest.getSession().setAttribute("oldUrlParams", nowUrlParams);
                    return false;
                }

            }
        } catch (Exception e) {
            e.printStackTrace();
            // 此处是我自定义异常
            throw new BusinessException("验证是否为重复请求时出错了!");
        }
    }
7.3 配置拦截器

xml:

<!-- 自定义相同url和数据的拦截器 拦截所有的url -->
<mvc:interceptors>
      <mvc:interceptor>
          <!-- 拦截url -->
          <mvc:mapping path="/**"/>
          <!-- 自定义拦截器类 -->
          <bean class="com.engraver.framework.interceptor.SameUrlDataInterceptor"/>
      </mvc:interceptor>
</mvc:interceptors>

springboot:

  1. 新建的 config 包,用来装初始化文件,在配置之下新建 WebConfigurer
  2. WebConfigurer需要实现 WebMvcConfigurer 这个接口,并实现里面的两个方法。(在老版本的 spring-boot 中使用的是WebMvcConfigurerAdapter,新版本中已过时!!!还有不能通过继承 WebMvcConfigurationSupport 这个类来实现,这样会在某些情况下失效!!!),第二个 addInterceptors 方法用来注册添加拦截器。
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
@Configuration
public class WebConfigurer implements WebMvcConfigurer {
 
  // 这个方法是用来配置静态资源的,比如html,js,css,等等
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
  }
 
  // 这个方法用来注册拦截器,我们自己写好的拦截器需要通过这里添加注册才能生效
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
  }
}
  1. 在WebConfigurer中添加拦截器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
@Configuration
public class WebConfigurer implements WebMvcConfigurer {
 
  @Autowired
  private SameUrlDataInterceptor sameUrlDataInterceptor ;
 
  // 这个方法是用来配置静态资源的,比如html,js,css,等等
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
  }
 
  // 这个方法用来注册拦截器,我们自己写好的拦截器需要通过这里添加注册才能生效
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    // addPathPatterns("/**") 表示拦截所有的请求,
    // excludePathPatterns("/login", "/register") 表示除了登陆之外,都可以防止表单重复提交
    registry.addInterceptor(sameUrlDataInterceptor ).addPathPatterns("/**").excludePathPatterns("/login");
  }
}

  1. 使用方法
    在controller类的方法上增加@PreventRepeat注解

部分内容转自博客:
https://blog.csdn.net/Huozhiwu_11/article/details/78742886
https://www.cnblogs.com/huanghuizhou/p/9153837.html
https://blog.csdn.net/qq_30745307/article/details/80974407
https://www.jianshu.com/p/e8e51b3a9371

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值