Fork me on GitHub

使用自定义注解实现接口防重复提交

  后端实现防重复提交的方式有很多中,大颗粒级别可以使用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);
    }
}

 

posted @ 2022-08-23 17:35  JackpotHan  阅读(399)  评论(0编辑  收藏  举报