【Springboot】防止表单重复提交注解(前后端分离&单节点)

文章目录

  • 前言
  • 装备
  • Core-Code
    • 新增注解@AvoidDuplicateFormToken
    • 异常处理
    • 缓存类
    • 自定义表单拦截器
    • SpringBoot配置拦截器
    • Controller使用
  • 总结
    • 注意点
    • 个人建议
  • github
  • 作者

前言

最近先更新微服务和web相关。大数据后补

SpringBoot防止表单重复提交。基于拦截器对带注解的请求进行拦截,处理。

后面总结一下为什么要如此使用。

应用场景:

  1. 使用浏览器后退按钮重复之前的操作,导致重复提交表单。重要业务会导致很重大问题,例如最常见的下单场景。下两个单,计算的金额就不一样了。

  2. 我们的程序那么忙也没必要处理重复的HTTP请求。

注意:

  1. 单节点(多节点的不适用)
  2. 前后端分离(前后端不分离的更简单。后面说)

装备

  • SpringBoot 2.0.3

Core-Code

新增注解@AvoidDuplicateFormToken

/**
 * 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(); } }

自定义表单拦截器

  1. 情况一:单节点应用
  2. 情况二:前后端分离==>这个时候前端一般通过请求头传入经过校验的UserToken
  3. 情况三:前后端不分离==>通过userId获取到用户信息保存到session进行校验

下面的情况是前后端分离。

前后端不分离很简单。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; } }

SpringBoot配置拦截器

/**
 * 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/**"); } }

Controller使用

	@AvoidDuplicateFormToken
    @GetMapping("/test")
    public ResponseEntity<?> test() {
        return null;
    }

总结

注意点

  1. SpringBoot2.x使用的是implements WebMvcConfigurer{}实现拦截器功能

  2. 【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

  3. 有的朋友说为什么不能用hashmap来做存储。这里我反问一句什么时候remove呢?我们没法控制,最好的实施方案就是用echache,配置expireTime超时时间。

  4. 这个方案是单节点的。分布式的时候我们可以用redis,弱一点的甚至用database等都可以,重点是记录下来token。

个人建议

  1. 防止表单重复提交要根据业务做。不需要每个系统都有这个功能。最经典场面就是购物车提交订单。实际业务才是我们定制功能和架构的基准
  2. 不管是否使用表单重复提交,我们数据库要进行唯一约束,也是解决一般重复提交的问题。

github

https://github.com/ithuhui/hui-base-java

在【hui-base-common】下面的com.hui.base.common.interceptor

作者

 作者:HuHui
 转载:欢迎一起讨论web和大数据问题,转载请注明作者和原文链接,感谢

你可能感兴趣的:(JavaWeb,Developer,Manual)