赞
踩
我们在平时项目开发中可能会出现下面这些情况:
这些情况都会导致表单重复提交,造成数据重复。因此,我们需要对这些情况进行一定的预防。
使用js防止表单重复提交的方式,其实就是设置一个全局的标记。在表单提交后,修改该标记的值。从而对重复提交进行预防。但是,并不推荐这种方式,该方法存在存在下列优缺点:
优点:
缺点:
代码示例:
<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>
在数据库里添加唯一约束或创建唯一索引,防止出现重复数据。这是最有效的防止重复提交数据的方法。但是,通过数据库加唯一键约束能有效避免数据库重复插入相同数据。但无法阻止恶意用户重复提交表单(攻击网站),服务器大量执行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");
}
在提交后执行页面重定向,这就是所谓的Post-Redirect-Get (PRG)模式。简言之,当用户提交了表单后,你去执行一个客户端的重定向,转到提交成功信息页面。这能避免用户按F5导致的重复提交,而其也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样问题。
用户提交表单之后,执行重定向,转到成功信息页面。可避免用户按F5刷新页面和点击浏览器前进或后退导致的重复提交。
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);
}
在服务器端生成一个唯一的随机标识号,专业术语称为Token,同时在当前用户的Session域中保存这个Token。然后将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端,然后在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的标识号。
在下列情况下,服务器程序将拒绝处理用户提交的表单请求:
示例代码如下:
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); } }
<%@ 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>
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); } }
实现步骤:
(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; } }
以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,该方法将不起任何作用,这点请注意。
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 {
}
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("验证是否为重复请求时出错了!"); } }
xml:
<!-- 自定义相同url和数据的拦截器 拦截所有的url -->
<mvc:interceptors>
<mvc:interceptor>
<!-- 拦截url -->
<mvc:mapping path="/**"/>
<!-- 自定义拦截器类 -->
<bean class="com.engraver.framework.interceptor.SameUrlDataInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
springboot:
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) { } }
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"); } }
部分内容转自博客:
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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。