首页 > 其他分享 >使用自定义注解实现接口防重复提交

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

时间:2022-08-23 17:47:36浏览次数:103  
标签:Map swaggerProperties REPEAT return String 自定义 接口 注解 public

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

 

标签:Map,swaggerProperties,REPEAT,return,String,自定义,接口,注解,public
From: https://www.cnblogs.com/JackpotHan/p/16617173.html

相关文章

  • 树莓派搭建WordPress博客:为博客网站配置自定义域名 11/11
    在上一篇文章中,我们通过在服务器上设置新的域名,并将新域名的指向引导到cpolar的数据通道后台入口端,让新域名的链接生效。接下来,我们就要在本地树莓派端,对cpolar进行设置,让c......
  • .net core 获取接口访问类型
     //注册服务usingMicrosoft.AspNetCore.Http;services.AddSingleton<IHttpContextAccessor,HttpContextAccessor>();  //实现层//注入引用privatere......
  • 自定义Mybatis拦截器实现自动添加创建人修改人等公共字段
    摘要本文通过自定义Mybatis拦截器拦截Executor接口实现在插入和修改操作时自动添加创建人修改人等公共字段,话不多说,直接上代码定义Mybatis拦截器packagecom.syb.spring......
  • lombok @Builder注解
    https://blog.csdn.net/baidu_35085676/article/details/89193416?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%......
  • VUE学习-自定义修饰符
    自定义修饰符组件constmyComponent={template:`<inputtype="text":value="modelValue"@input="emitValue"/>`,props:{modelValue:String,......
  • 【SpringBoot】自定义注解实现yml格式配置文件注入
    1.创建一个starter项目(非必须,主要更好分离代码)2.创建注解文件@YamlSource@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Componentpu......
  • vue自定义指令的使用
    1、背景:想通过自定义指令v-hasHelp控制页面右上角是否出现帮助按钮,点击按钮可以跳转外部链接。用自定义指令的目的是方便。2、先在自己的项目中注册使用hasHelpindex.j......
  • 抽象和接口
    抽象类abstract,抽象方法,只有方法名字,没有方法实现抽象类不能被实例化,不能new这个抽象类,只是个约束!!newAction();会报错抽象类的子类,必须实现抽象类中的方法;(抽象......
  • Win 10 中通过 VMWare 16 在 UEFI 引导模式下安装 Ubuntu 18.04 虚拟机并自定义分区
    本文使用 ZhihuOnVSCode 创作并发布 VMWare安装虚拟机时默认按照Legacy引导模式(传统BIOS)进行,无法充分发挥系统及硬件性能,本文旨在记录在Win10中通过VMWare......
  • java中的注解和反射
    1什么是注解(1)定义:Annotation是从Jdk5.0开始后才引入的,不是程序本身,可以对程序作出解释。可以被其他程序读取(2)注解的使用:可以在package,class,method,field上面使用,......