最近先更新微服务和web相关。大数据后补
SpringBoot防止表单重复提交。基于拦截器对带注解的请求进行拦截,处理。
后面总结一下为什么要如此使用。
应用场景:
使用浏览器后退按钮重复之前的操作,导致重复提交表单。重要业务会导致很重大问题,例如最常见的下单场景。下两个单,计算的金额就不一样了。
我们的程序那么忙也没必要处理重复的HTTP请求。
注意:
/**
* AvoidDuplicateSubmit
*
* Description: 防止表单重复提交注解
*
* Creation Time: 2018/11/28 19:27.
*
* @author Hu weihui
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AvoidDuplicateFormToken {
}
/**
* FormTokenException
*
* Description:表单提交异常处理
*
* Creation Time: 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;
}
/**
* FormExceptionEnum
*
* Description: 表单提交异常处理枚举类
*
* Creation Time: 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;
}
}
/**
* ErrorConstant
*
* Description: 异常常量
*
* Creation Time: 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 = "参数校验异常";
}
/**
* UserCache
*
* Description:
*
* Creation Time: 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
/**
* DuplicateSubmitInterceptor
*
* Description: 表单重复提交拦截器(单节点,前后端分离情况)
* 前后端分离->前端请求头传入USER_TOKEN
* 前后端不分离->用户信息保存在Session
*
* Creation Time: 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;
}
}
/**
* WebConfig
*
* Description:
*
* Creation Time: 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。
https://github.com/ithuhui/hui-base-java
在【hui-base-common】下面的com.hui.base.common.interceptor
作者:HuHui
转载:欢迎一起讨论web和大数据问题,转载请注明作者和原文链接,感谢