【SpringBoot】防止表单重复提交注解(前后端分离&单节点)
文章目录前言装备Core-Code新增注解@AvoidDuplicateFormToken异常处理自定义表单拦截器SpringBoot配置拦截器Controller使用总结注意点个人建议作者前言最近先更新微服务和web相关。大数据后补SpringBoot防止表单重复提交。基于拦截器对带注解的请求进行拦截,处理。后面总结一下为什么要如此使用。应用场景:使用浏览器后退按钮重复之前的操作...
文章目录
Note
最近先更新微服务和web相关。大数据后补
SpringBoot防止表单重复提交。基于拦截器对带注解的请求进行拦截,处理。
后面总结一下为什么要如此使用。
应用场景:
-
使用浏览器后退按钮重复之前的操作,导致重复提交表单。重要业务会导致很重大问题,例如最常见的下单场景。下两个单,计算的金额就不一样了。
-
我们的程序那么忙也没必要处理重复的HTTP请求。
注意:
- 单节点(多节点的不适用)
- 前后端分离(前后端不分离的更简单。后面说)
Github
地址:https://github.com/ithuhui/hui-base-java
分支:master
模块:【hui-base-common】
位置:com.hui.base.common.interceptor
Ready
- maven
- IDEA
- SpringBoot 2.0.3
Core-Code
新增注解@AvoidDuplicateFormToken
/**
* <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();
}
}
自定义表单拦截器
- 情况一:单节点应用
- 情况二:前后端分离==>这个时候前端一般通过请求头传入经过校验的UserToken
- 情况三:前后端不分离==>通过userId获取到用户信息保存到session进行校验
下面的情况是前后端分离。
前后端不分离很简单。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;
}
}
SpringBoot配置拦截器
/**
* <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/**");
}
}
Controller使用
@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和大数据问题,转载请注明作者和原文链接,感谢
更多推荐
所有评论(0)