Preventing repeated submission of requests: Implementation plan of annotations + interceptors

Article directory

    • Learn about requesting duplicate submissions
    • Solutions
    • Implementation

Learn about repeated submissions

Repeated submission of requests means that the user repeatedly submits the same request before a request is processed. This situation usually occurs due to network delay, user misoperation or poor system performance.

Requesting duplicate submissions may cause the following issues and impacts:

  1. Data inconsistency: If repeatedly submitted requests include modification operations to data, it may lead to data inconsistencies, such as repeated purchase of goods, repeated payment of orders, etc.

  2. Waste of system resources: Repeatedly submitted requests will occupy system resources, causing problems such as excessive server load and slow response time, affecting system performance and user experience.

  3. Security issues: If certain sensitive operations (such as payment, password change, etc.) are submitted repeatedly, they may cause security issues, such as repeated payments leading to loss of funds, repeated password changes leading to account theft, etc.

Therefore, preventing repeated submission of requests is an important measure to ensure system data consistency, improve system performance, and ensure system security.

Solution ideas

In Spring Boot, we can effectively prevent users from repeatedly submitting forms or requests in a short period of time. We can combine it with Redis to achieve this function. We can store the user’s request information in the cache, so that we can track the status of the user’s request in real time. It can also improve system performance. In order to limit users from repeatedly submitting the same request within a short period of time, we can set a time interval to limit repeated submissions. When the user submits the same request within the specified time interval, an error message will be returned, otherwise the request will be processed normally. Therefore, through this method, we can effectively prevent users from submitting repeated requests in a short period of time and avoid wasting system resources. Normally, we can use annotations + interceptors to achieve this function.

Annotations are a way of adding metadata to your code, which can add extra information to methods, classes, fields, etc. In scenarios where requests are submitted repeatedly, we can add custom annotations to methods or interfaces, obtain and parse the annotation information through the reflection mechanism at runtime, and execute the corresponding logic.

An interceptor is a component that processes requests before they reach the controller. It can intercept requests and perform some pre- or post-processing operations before or after the request reaches the target handler.

The specific work flow chart is as follows:

image-20231028152416711

Detailed implementation

  1. First we need to create a custom annotation to mark methods that need to intercept repeated requests. For example, we create an annotation named @RepeatSubmit in the project:

    @Inherited
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RepeatSubmit {<!-- -->
    
        /**
         * Interval time (ms), less than this time is considered a repeated submission
         */
        public int interval() default 60000;
    
        /**
         * Prompt message
         */
        public String message() default "Repeated submissions are not allowed, please try again later";
    
    }
    
  2. Create an interceptor class to intercept requests annotated with @RepeatSubmit. In the interceptor, you can determine whether the request has been processed by saving the unique identifier of the request (such as a token).

    @Component
    public class RepeatSubmitInterceptor implements HandlerInterceptor {<!-- -->
    
        @Autowired
        private RedisCache redisCache;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {<!-- -->
    
            // Determine whether the processor object is of HandlerMethod type
            if (handler instanceof HandlerMethod) {<!-- -->
                HandlerMethod handlerMethod = (HandlerMethod) handler;
                Method method = handlerMethod.getMethod();
    
                // Get the RepeatSubmit annotation on the method
                RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
    
                if (annotation != null) {<!-- -->
                    // Get the requested IP address, URI, request method, request parameters and request body
                    String ipAddr = IpUtils.getIpAddr(request);
                    String requestURI = request.getRequestURI();
                    String requestMethod = request.getMethod();
                    Map<String, String> requestParams = ServletUtils.getRequestParams(request);
                    Map<String, String> requestBody = ServletUtils.getRequestBody(request);
    
                    //Create a cache data object and store the request information in it
                    Map<String,Object> cacheData = new HashMap<>();
                    cacheData.put("ip",ipAddr);
                    cacheData.put("uri",requestURI);
                    cacheData.put("method",requestMethod);
                    cacheData.put("params",requestParams);
                    cacheData.put("body",requestBody);
    
                    // Encrypt the cached data using MD5
                    String md5V = MD5Utils.encrypt(cacheData);
    
                    //Set the cache object in the Redis cache, return false if it already exists
                    Boolean b = redisCache.setCacheObjectIfAbsent("repeat_submit:" + ipAddr + ":" + md5V, cacheData, annotation.interval(), TimeUnit.MILLISECONDS);
    
                    if(b){<!-- -->
                        // If the cache setting is successful, release the request
                        return true;
                    }else {<!-- -->
                        // If the cache already exists, return a duplicate submission error message
                        AjaxResult ajaxResult = AjaxResult.error(annotation.message());
                        ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
                        return false;
                    }
                }
    
                // If the method does not have RepeatSubmit annotation, release the request
                return true;
            }else {<!-- -->
                // If the handler object is not of HandlerMethod type, release the request
                return true;
            }
        }
    
    }
    
  3. Register the interceptor in Spring Boot’s configuration class:

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {<!-- -->
    
        @Autowired
        private RepeatSubmitInterceptor repeatSubmitInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {<!-- -->
            registry
                .addInterceptor(repeatSubmitInterceptor)
                .addPathPatterns("/**");
        }
        
    }
    
  4. Add the @DuplicateSubmitToken annotation to the method that needs to intercept repeated requests:

    @Controller
    public class MyController {<!-- -->
    
        @RequestMapping("/submit")
        @RepeatSubmit
        public String submit() {<!-- -->
            // Process request logic
            return "success";
        }
    }
    
  5. Start the project, use ApiFox to initiate a request to access the /submit interface, and the request is sent successfully:

    image-20231028144719930

  6. After the request is sent successfully, check the cache data in Redis, including the interface data when we requested:

    image-20231028144758402

  7. Initiate the same request again and directly return the prompt message “Repeated submission is not allowed, please try again later”:

    image-20231028144902473