前言
在Web / App项目中,有一些请求或操作会对数据产生影响(比如新增、删除、修改),针对这类请求一般都需要做一些保护,以防止用户有意或无意的重复发起这样的请求导致的数据错乱。
常见处理方案
1.客户端
例如表单提交后将提交按钮设为disable 等等方法...
2.服务端
前端的限制仅能解决少部分问题,且不够彻底,后端自有的防重复处理措施必不可少,义不容辞。
在此提供一个我在项目中用到的方案。简单来说就是判断请求url和数据是否和上一次相同。
方法步骤
1.主要逻辑:
给所有的url加一个拦截器,每次请求将url存入session,下次请求验证url数据是否相同,相同则拒绝访问。
当然,我在此基础上做了一些优化,比如:
使用session有局限性,用户量大了以后服务器会撑不住,在此我使用了redis来替换。
加入了token令牌机制。
2.实现步骤:
- 2.1自定义一个注解
-
1 /** 2 * @Title: SameUrlData 3 * @Description: 自定义注解防止表单重复提交 4 * @Auther: xhq 5 * @Version: 1.0 6 * @create 2019/3/26 10:43 7 */ 8 @Inherited 9 @Target(ElementType.METHOD) 10 @Retention(RetentionPolicy.RUNTIME) 11 @Documented 12 public @interface SameUrlData { 13 14 }
- 2.2自定义拦截器类
- 检查此接口调用的方法是否使用了SameUrlData注解,若没有使用,表示此接口不需要校验;
- 若使用了注解,获取请求url+参数,并去除一直在变化的参数(比如时间戳timeStamp和签名sign)
- 检查参数中是否有token参数(token代表不同的用户的唯一标识),没有直接放行
- 有token参数,将token+url作为redis的key,url+参数作为value存入redis,并设定自动销毁时间
- (此处如果项目中没有redis,可参照我的另外一篇博客可解决:https://www.cnblogs.com/xhq1024/p/11115755.html)
- 再次访问进行验证是否重复请求
-
1 import com.alibaba.fastjson.JSONObject; 2 import com.tuohang.hydra.framework.common.spring.SpringKit; 3 import com.tuohang.hydra.toolkit.basis.string.StringKit; 4 import org.slf4j.Logger; 5 import org.slf4j.LoggerFactory; 6 import org.springframework.data.redis.core.StringRedisTemplate; 7 import org.springframework.stereotype.Component; 8 import org.springframework.web.method.HandlerMethod; 9 import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 10 11 import javax.servlet.http.HttpServletRequest; 12 import javax.servlet.http.HttpServletResponse; 13 import java.lang.reflect.Method; 14 import java.util.HashMap; 15 import java.util.Iterator; 16 import java.util.Map; 17 import java.util.concurrent.TimeUnit; 18 19 /** 20 * @Title: 防止用户重复提交数据拦截器 21 * @Description: 将用户访问的url和参数结合token存入redis,每次访问进行验证是否重复请求接口 22 * @Auther: xhq 23 * @Version: 1.0 24 * @create 2019/3/26 10:35 25 */ 26 @Component 27 public class SameUrlDataInterceptor extends HandlerInterceptorAdapter { 28 29 private static Logger LOG = LoggerFactory.getLogger(SameUrlDataInterceptor.class); 30 31 /** 32 * 是否阻止提交,fasle阻止,true放行 33 * @return 34 */ 35 @Override 36 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 37 if (handler instanceof HandlerMethod) { 38 HandlerMethod handlerMethod = (HandlerMethod) handler; 39 Method method = handlerMethod.getMethod(); 40 SameUrlData annotation = method.getAnnotation(SameUrlData.class); 41 if (annotation != null) { 42 if(repeatDataValidator(request)){ 43 //请求数据相同 44 LOG.warn("please don't repeat submit,url:"+ request.getServletPath()); 45 JSONObject result = new JSONObject(); 46 result.put("statusCode","500"); 47 result.put("message","请勿重复请求"); 48 response.setCharacterEncoding("UTF-8"); 49 response.setContentType("application/json; charset=utf-8"); 50 response.getWriter().write(result.toString()); 51 response.getWriter().close(); 52 // 拦截之后跳转页面 53 // String formRequest = request.getRequestURI(); 54 // request.setAttribute("myurl", formRequest); 55 // request.getRequestDispatcher("/WebRoot/common/error/jsp/error_message.jsp").forward(request, response); 56 return false; 57 }else {//如果不是重复相同数据 58 return true; 59 } 60 } 61 return true; 62 } else { 63 return super.preHandle(request, response, handler); 64 } 65 } 66 /** 67 * 验证同一个url数据是否相同提交,相同返回true 68 * @param httpServletRequest 69 * @return 70 */ 71 public boolean repeatDataValidator(HttpServletRequest httpServletRequest){ 72 //获取请求参数map 73 Map<String, String[]> parameterMap = httpServletRequest.getParameterMap(); 74 Iterator<Map.Entry<String, String[]>> it = parameterMap.entrySet().iterator(); 75 String token = ""; 76 Map<String, String[]> parameterMapNew = new HashMap<>(); 77 while(it.hasNext()){ 78 Map.Entry<String, String[]> entry = it.next(); 79 if(!entry.getKey().equals("timeStamp") && !entry.getKey().equals("sign")){ 80 //去除sign和timeStamp这两个参数,因为这两个参数一直在变化 81 parameterMapNew.put(entry.getKey(), entry.getValue()); 82 if(entry.getKey().equals("token")) { 83 token = entry.getValue()[0]; 84 } 85 } 86 } 87 if (StringKit.isBlank(token)){ 88 //如果没有token,直接放行 89 return false; 90 } 91 //过滤过后的请求内容 92 String params = JSONObject.toJSONString(parameterMapNew); 93 94 System.out.println("params==========="+params); 95 96 String url = httpServletRequest.getRequestURI(); 97 Map<String,String> map = new HashMap<>(); 98 //key为接口,value为参数 99 map.put(url, params); 100 String nowUrlParams = map.toString(); 101 102 StringRedisTemplate smsRedisTemplate = SpringKit.getBean(StringRedisTemplate.class); 103 String redisKey = token + url; 104 String preUrlParams = smsRedisTemplate.opsForValue().get(redisKey); 105 if(preUrlParams == null){ 106 //如果上一个数据为null,表示还没有访问页面 107 //存放并且设置有效期,2秒 108 smsRedisTemplate.opsForValue().set(redisKey, nowUrlParams, 2, TimeUnit.SECONDS); 109 return false; 110 }else{//否则,已经访问过页面 111 if(preUrlParams.equals(nowUrlParams)){ 112 //如果上次url+数据和本次url+数据相同,则表示重复添加数据 113 return true; 114 }else{//如果上次 url+数据 和本次url加数据不同,则不是重复提交 115 smsRedisTemplate.opsForValue().set(redisKey, nowUrlParams, 1, TimeUnit.SECONDS); 116 return false; 117 } 118 } 119 } 120 }
- 2.3注册拦截器
1 @Configuration 2 public class WebMvcConfigExt extends WebMvcConfig { 3 4 /** 5 * 防止重复提交拦截器 6 */ 7 @Autowired 8 private SameUrlDataInterceptor sameUrlDataInterceptor; 9 10 @Override 11 public void addInterceptors(InterceptorRegistry registry) { 12 // 避开静态资源 13 List<String> resourcePaths = defineResourcePaths(); 14 registry.addInterceptor(sameUrlDataInterceptor).addPathPatterns("/**").excludePathPatterns(resourcePaths);// 重复请求 15 } 16 17 /** 18 * 自定义静态资源路径 19 * 20 * @return 21 */ 22 @Override 23 public List<String> defineResourcePaths() { 24 List<String> patterns = new ArrayList<>(); 25 patterns.add("/assets/**"); 26 patterns.add("/upload/**"); 27 patterns.add("/static/**"); 28 patterns.add("/common/**"); 29 patterns.add("/error"); 30 return patterns; 31 } 32 }
- 在相应方法上加@SameUrlData注解
@SameUrlData @ResponseBody @RequestMapping(value = "/saveOrUpdate") public String saveOrUpdate(){ }