赞
踩
最近先更新微服务和web相关。大数据后补
SpringBoot防止表单重复提交。基于拦截器对带注解的请求进行拦截,处理。
后面总结一下为什么要如此使用。
应用场景:
使用浏览器后退按钮重复之前的操作,导致重复提交表单。重要业务会导致很重大问题,例如最常见的下单场景。下两个单,计算的金额就不一样了。
我们的程序那么忙也没必要处理重复的HTTP请求。
注意:
地址:https://github.com/ithuhui/hui-base-java
分支:master
模块:【hui-base-common】
位置:com.hui.base.common.interceptor
/**
* <b><code>AvoidDuplicateSubmit</code></b>
* <p/>
* Description: 防止表单重复提交注解
* <p/>
* <b>Creation Time:</b> 2018/11/28 19:27.
*
* @author Hu weihui
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AvoidDuplicateFormToken {
}
/** * <b><code>FormTokenException</code></b> * <p/> * Description:表单提交异常处理 * <p/> * <b>Creation Time:</b> 2018/12/3 15:26. * * @author Hu weihui */ public class FormTokenException extends RuntimeException{ private static final long serialVersionUID = 512936007428810210L; private String errorCode; private String errorMsg; public FormTokenException(String errorCode,String errorMsg) { super(errorMsg); this.errorCode = errorCode; } public FormTokenException(String errorCode,String errorMsg,Throwable cause) { super(errorMsg,cause); this.errorCode = errorCode; } public FormTokenException(FormTokenExceptionEnum formTokenExceptionEnum) { super(formTokenExceptionEnum.getErrorMsg()); this.errorCode = formTokenExceptionEnum.getErrorCode(); } public FormTokenException(FormTokenExceptionEnum formTokenExceptionEnum,Throwable cause) { super(formTokenExceptionEnum.getErrorMsg(),cause); this.errorCode = errorCode; }
/** * <b><code>FormExceptionEnum</code></b> * <p/> * Description: 表单提交异常处理枚举类 * <p/> * <b>Creation Time:</b> 2018/11/29 14:15. * * @author Hu weihui */ @Getter public enum FormTokenExceptionEnum { DUPLICATE_SUBMIT("FT-001", ErrorConstant.NETWORK_ERROR, "表单重复提交"), ILLEGAL_SUBMIT("FT-002",ErrorConstant.NETWORK_ERROR,"非法提交表单"), SERVER_TOKEN_ERROR("FT-003",ErrorConstant.NETWORK_ERROR,"服务端未接收到请求"), UNKONW_ERROR("FT-004", ErrorConstant.NETWORK_ERROR, "表单提交未知错误"); private String errorCode; private String errorType; private String errorMsg; FormTokenExceptionEnum(String errorCode, String errorType, String errorMsg) { this.errorCode = errorCode; this.errorType = errorType; this.errorMsg = errorMsg; } }
/** * <b><code>ErrorConstant</code></b> * <p/> * Description: 异常常量 * <p/> * <b>Creation Time:</b> 2018/12/3 15:28. * * @author Hu weihui */ public class ErrorConstant { public static final String SYSTEM_ERROR = "系统异常"; public static final String UNKNOW_ERROR = "未知异常"; public static final String NETWORK_ERROR = "网络异常"; public static final String BUSINESS_ERROR = "业务异常"; public static final String VALID_ERROR = "参数校验异常"; }
/** * <b><code>UserCache</code></b> * <p/> * Description: * <p/> * <b>Creation Time:</b> 2018/12/3 11:00. * * @author Hu weihui */ public class UserCache { /** * 表单重复提交cache,有效期2秒. * * @return the cache * @author : Hu weihui */ @Bean public Cache<String,String> getUserCache(){ return CacheBuilder.newBuilder().expireAfterAccess(2L,TimeUnit.SECONDS).build(); } }
下面的情况是前后端分离。
前后端不分离很简单。request.getSession()做后续操作就OK
/** * <b><code>DuplicateSubmitInterceptor</code></b> * <p/> * Description: 表单重复提交拦截器(单节点,前后端分离情况) * 前后端分离->前端请求头传入USER_TOKEN * 前后端不分离->用户信息保存在Session * <p/> * <b>Creation Time:</b> 2018/12/3 14:25. * * @author Hu weihui */ @Slf4j public class DuplicateSubmitInterceptor extends HandlerInterceptorAdapter { private static final String USER_TOKEN_KEY = "token"; @Autowired private Cache<String, String> cache; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof ResourceHttpRequestHandler) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); AvoidDuplicateFormToken annotation = method.getAnnotation(AvoidDuplicateFormToken.class); //查看是否有注解 if (annotation != null) { boolean result = !isDuplicateSubmit(request); return result; } return super.preHandle(request, response, handler); } /** * 判断是否重复提交表单. * * @param request the request * @return the boolean * @author : Hu weihui */ private boolean isDuplicateSubmit(HttpServletRequest request) { try { //请求头是否有token,没有则为非法提交 String userToken = request.getHeader(USER_TOKEN_KEY); if (StringUtils.isEmpty(userToken)) { throw new FormTokenException(FormTokenExceptionEnum.ILLEGAL_SUBMIT); } String clientoken = cache.getIfPresent(userToken); //查看cache内是否有token,token2秒内清除,有则为重复提交 if (null != clientoken){ log.info("表单重复提交:用户token: {},表单token: {}", userToken); throw new FormTokenException(FormTokenExceptionEnum.DUPLICATE_SUBMIT); }else { //没有token则当做首次/二次提交,记录在cache cache.put(userToken,UUID.randomUUID().toString()); } } catch (Exception e) { log.info("重复提交表单拦截器错误,{}", e.getMessage()); throw new FormTokenException(FormTokenExceptionEnum.SERVER_TOKEN_ERROR); } return false; } }
/** * <b><code>WebConfig</code></b> * <p/> * Description: * <p/> * <b>Creation Time:</b> 2018/12/3 15:31. * * @author Hu weihui */ public class WebConfig implements WebMvcConfigurer { //新增拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new DuplicateSubmitInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**"); } }
@AvoidDuplicateFormToken
@GetMapping("/test")
public ResponseEntity<?> test() {
return null;
}
SpringBoot2.x使用的是implements WebMvcConfigurer{}实现拦截器功能
【DuplicateSubmitInterceptor】
HandlerMethod handlerMethod = (HandlerMethod) handler;报错
java.lang.ClassCastException: org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler cannot be cast to org.springframework.web.method.HandlerMethod
当请求里面还带有其他的类型请求的时候,而且不是你配置的拦截的规则,那么它转换类型的时候就报错了,这里明显就是因为swagger的静态资源匹配请求的问题了。
这个方法会默认当做处理静态资源,因此需要排除
.excludePathPatterns("/swagger-resources/", "/webjars/", “/v2/", "/swagger-ui.html/”);
这里参考了,这个朋友的源码分析,十分感谢:https://yq.aliyun.com/articles/515182
有的朋友说为什么不能用hashmap来做存储。这里我反问一句什么时候remove呢?我们没法控制,最好的实施方案就是用echache,配置expireTime超时时间。
这个方案是单节点的。分布式的时候我们可以用redis,弱一点的甚至用database等都可以,重点是记录下来token。
作者:HuHui
转载:欢迎一起讨论web和大数据问题,转载请注明作者和原文链接,感谢
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。