使用自定义注解实现接口防重复提交
后端实现防重复提交的方式有很多中,大颗粒级别可以使用Redis或nginx,也就是所谓的滑动窗口、令牌桶等,但是这些大颗粒只能实现同一接口同一IP同一用户的重复提交,不能对请求参数进行校验(当然可以通过编码的方式处理掉)。
本文介绍的方案前提是:所有请求不包含时间戳、不对请求进行加解密,即所有的接口参数全部明文且全部为业务参数(当然包含请求头的数据).
代码采用的是最简单的方式,没有使用线程池、没有使用发布订阅。感兴趣的同学可以进行优化。
以下是方案的全部代码
自定义注解 RepeatSubmit
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RepeatSubmit { }
mvc配置(项目中使用了swagger,在此统一配置)@EnableOpenApi
@Configuration public class MyMvcConfig implements WebMvcConfigurer { private final SwaggerProperties swaggerProperties; private final RepeatSubmitInterceptor repeatSubmitInterceptor; /** * 首页地址 */ @Value("${shiro.user.indexUrl}") private String indexUrl; public SwaggerConfig(SwaggerProperties swaggerProperties, RepeatSubmitInterceptor repeatSubmitInterceptor) { this.swaggerProperties = swaggerProperties; this.repeatSubmitInterceptor = repeatSubmitInterceptor; } @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("forward:" + indexUrl); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //swagger配置 registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); } @Bean public Docket createRestApi() { return new Docket(DocumentationType.OAS_30).pathMapping("/") // 定义是否开启swagger,false为关闭,可以通过变量控制 .enable(swaggerProperties.getEnable()) // 将api的元信息设置为包含在json ResourceListing响应中。 .apiInfo(apiInfo()) // 接口调试地址 .host(swaggerProperties.getTryHost()) // 选择哪些接口作为swagger的doc发布 .select() .apis(RequestHandlerSelectors.any()) .paths(PathSelectors.any()) .build() .globalRequestParameters( singletonList(new springfox.documentation.builders.RequestParameterBuilder() .name("Authentication") .description("token") .in(ParameterType.HEADER) .required(true) .query(q -> q.model(m -> m.scalarModel(ScalarType.STRING))) .build())) // 支持的通讯协议集合 .protocols(newHashSet("https", "http")); } /** * API 页面上半部分展示信息 */ private ApiInfo apiInfo() { return new ApiInfoBuilder().title(swaggerProperties.getApplicationName() + " Api Doc") .description(swaggerProperties.getApplicationDescription()) .version("Application Version: " + swaggerProperties.getApplicationVersion() + ", Spring Boot Version: " + SpringBootVersion.getVersion()) .build(); } @SafeVarargs private <T> Set<T> newHashSet(T... ts) { if (ts.length > 0) { return new LinkedHashSet<>(Arrays.asList(ts)); } return Collections.emptySet(); } /** * 通用拦截器排除swagger设置,所有拦截器都会自动加swagger相关的资源排除信息 */ @SuppressWarnings("unchecked") @Override public void addInterceptors(@NonNull InterceptorRegistry registry) { try {
//如果只使用自动注解实现防重复提交,只需要在interceptor注册表中注入repeatSubmitInterceptor即可 registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); Field registrationsField = FieldUtils.getField(InterceptorRegistry.class, "registrations", true); List<InterceptorRegistration> registrations = (List<InterceptorRegistration>) ReflectionUtils.getField(registrationsField, registry); if (registrations != null) { for (InterceptorRegistration interceptorRegistration : registrations) { interceptorRegistration .excludePathPatterns("/**/swagger**/**") .excludePathPatterns("/**/webjars/**") .excludePathPatterns("/**/v3/**") .excludePathPatterns("/**/doc.html"); } } } catch (Exception e) { e.printStackTrace(); } } }
实现HandlerInterceptor接口处理重复处理注解
@Component public class RepeatSubmitInterceptor implements HandlerInterceptor { public static final String REPEAT_PARAMS = "repeatParams"; public static final String REPEAT_TIME = "repeatTime"; public static final String SESSION_REPEAT_KEY = "repeatData"; /** * 间隔时间,单位:秒 默认3秒 * <p> * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据 */ private static final int INTERVAL_TIME = 3; /** * 秒与毫秒的进制转换 **/ private static final Long SEC_2_MILLIS = 1000L; @Override public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); if (annotation != null) { if (!this.isRepeatSubmit(request)) { return true; } throw new BizException(Res.msg(ResCode.DATA_REPEAT_ERROR, "不允许重复提交,请稍后再试")); } return true; } else { return HandlerInterceptor.super.preHandle(request, response, handler); } } @SuppressWarnings("unchecked") public boolean isRepeatSubmit(HttpServletRequest request) { // 本次参数及系统时间 String nowParams = JSON.toJSONString(request.getParameterMap()); Map<String, Object> nowDataMap = new HashMap<>(4); nowDataMap.put(REPEAT_PARAMS, nowParams); nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); // 请求地址(作为存放session的key值) String url = request.getRequestURI(); HttpSession session = request.getSession(); Object sessionObj = session.getAttribute(SESSION_REPEAT_KEY); if (sessionObj != null) { Map<String, Object> sessionMap = (Map<String, Object>) sessionObj; if (sessionMap.containsKey(url)) { Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url); if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap)) { return true; } } } Map<String, Object> sessionMap = new HashMap<>(4); sessionMap.put(url, nowDataMap); session.setAttribute(SESSION_REPEAT_KEY, sessionMap); return false; } /** * 判断参数是否相同 */ private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) { String nowParams = (String) nowMap.get(REPEAT_PARAMS); String preParams = (String) preMap.get(REPEAT_PARAMS); return nowParams.equals(preParams); } /** * 判断两次间隔时间 */ private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap) { long time1 = (Long) nowMap.get(REPEAT_TIME); long time2 = (Long) preMap.get(REPEAT_TIME); return (time1 - time2) < (INTERVAL_TIME * SEC_2_MILLIS); } }