If you analyze it according to the source code: prevent the form from repeatedly submitting @RepeatSubmit, RepeatableFilter, RepeatedlyRequestWrapper and RepeatSubmitInterceptor

Article directory

  • Summary
  • Configuration interceptor: WebMvcConfigurer
  • RepeatSubmit annotation
  • Interceptor specific implementation: RepeatSubmitInterceptor and SameUrlDataInterceptor
    • preHandle: Intercept processing before request processing.
    • To verify whether to submit repeatedly, the subclass implements specific anti-repeat submission rules
  • Solve the problem of reading parameters: HttpServletRequest and RepeatedlyRequestWrapper

Summary

Ruoyi is an open source background management system based on Spring Boot and MyBatis, which provides a series of interceptors (Interceptor) for processing requests. Among them, the RepeatSubmitInterceptor (repeated submission interceptor) is a key interceptor in the Raoyi system, which is used to prevent users from repeatedly submitting form requests.

In web applications, users may submit forms repeatedly, such as multiple clicks after clicking the submit button or network delays that cause users to mistakenly believe that the submission was unsuccessful and submit again. This can lead to problems such as duplicate data insertion or duplicate business logic processing.

The main function of RepeatSubmitInterceptor is to intercept and process the request when the user submits the form request to prevent repeated submission. Determine whether the url has a RepeatSubmit annotation, and if so, get it: [parameter, url, user] and put it in redis together with the expiration time in RepeatSubmit.

When I come back next time, I will go to redis to check. If it has expired, then pass it, and then put it back into redis. If it is still there, then it will not pass.

But there is a problem here, if the parameters are fetched from the body, then the stream will be read only once and cannot be read again. So before that, RepeatableFilter was used to make the encapsulated byte array Requestwrapper about this.

Configuration interceptor: WebMvcConfigurer

WebMvcConfigurer configures RepeatSubmitInterceptor:
RepeatSubmitInterceptor will be added to ResourcesConfig inherited from WebMvcConfigurer. The main method of RepeatSubmitInterceptor: this.isRepeatSubmit(request, annotation) is implemented by SameUrlDataInterceptor inherited from ResourcesConfig.

@Configuration
public class ResourcesConfig implements WebMvcConfigurer
{<!-- -->
    @Autowired
    private RepeatSubmitInterceptor repeatSubmitInterceptor;

    /**
     * Custom interception rules
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry)
    {<!-- -->
        registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
    }

}

RepeatSubmit annotation

Add the @RepeatSubmit annotation to the Controller method that needs to prevent repeated submissions. This annotation is provided by the Ruoyi framework to enable the repeated submission interceptor.
Only the @RepeatSubmit annotation is marked to prevent the form from being submitted repeatedly.

@Inherited
@Target(ElementType. METHOD)
@Retention(RetentionPolicy. RUNTIME)
@Documented
public @interface RepeatSubmit
{<!-- -->
    /**
     * Interval time (ms), less than this time is regarded as repeated submission
     */
    public int interval() default 5000;

    /**
     * Prompt message
     */
    public String message() default "Duplicate submission is not allowed, please try again later";
}

Interceptor specific implementation: RepeatSubmitInterceptor and SameUrlDataInterceptor

preHandle: Intercept processing before request processing.

It extracts the identity required for duplicate submissions from the request, and checks for duplicate submissions. If repeated submissions are detected, an error message can be returned or other processing methods can be adopted.

@Component
public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter
{<!-- -->
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {<!-- -->
        if (handler instanceof HandlerMethod)
        {<!-- -->
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod. getMethod();
            // Only the @RepeatSubmit annotation is marked to prevent the form from being submitted repeatedly, and other requests directly return true.
            RepeatSubmit annotation = method. getAnnotation(RepeatSubmit. class);
            if (annotation != null)
            {<!-- -->
                if (this.isRepeatSubmit(request, annotation))
                {<!-- -->
                    AjaxResult ajaxResult = AjaxResult. error(annotation. message());
                    ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
                    return false;
                }
            }
            return true;
        }
        else
        {<!-- -->
            return super. preHandle(request, response, handler);
        }
    }

    /**
     * To verify whether to submit repeatedly, the subclass implements specific anti-repeat submission rules
     *
     * @param request
     * @return
     * @throws Exception
     */
    public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
}

The core here is:

//Only if the @RepeatSubmit annotation is marked, it is necessary to prevent the form from being submitted repeatedly, and other requests directly return true.
RepeatSubmit annotation = method. getAnnotation(RepeatSubmit. class);

Verify whether to submit repeatedly and implement specific anti-repeat submission rules by subclass

The main method of RepeatSubmitInterceptor: this.isRepeatSubmit(request, annotation) is implemented by SameUrlDataInterceptor inherited from ResourcesConfig.
Determine whether the url has a RepeatSubmit annotation, if so, get it: [parameter, url, user] and put it in redis together with the expiration time in RepeatSubmit

@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{<!-- -->
    public final String REPEAT_PARAMS = "repeatParams";

    public final String REPEAT_TIME = "repeatTime";

    // Token custom ID
    @Value("${token. header}")
    private String header;

    @Autowired
    private RedisCache redisCache;

    @SuppressWarnings("unchecked")
    @Override
    public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation)
    {<!-- -->
        String nowParams = "";
        if (request instanceof RepeatedlyRequestWrapper)
        {<!-- -->
            RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
            nowParams = HttpHelper. getBodyString(repeatedlyRequest);
        }

        // The body parameter is empty, get the data of Parameter
        if (StringUtils. isEmpty(nowParams))
        {<!-- -->
            nowParams = JSONObject.toJSONString(request.getParameterMap());
        }
        Map<String, Object> nowDataMap = new HashMap<String, Object>();
        nowDataMap.put(REPEAT_PARAMS, nowParams);
        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());

        // request address (as the key value for storing the cache)
        String url = request. getRequestURI();

        // unique value (the request address is used if there is no message header)
        String submitKey = request. getHeader(header);
        if (StringUtils. isEmpty(submitKey))
        {<!-- -->
            submitKey = url;
        }

        // unique identifier (specified key + message header)
        String cacheRepeatKey = Constants. REPEAT_SUBMIT_KEY + submitKey;

        Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
        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) & amp; & amp; compareTime(nowDataMap, preDataMap, annotation.interval()))
                {<!-- -->
                    return true;
                }
            }
        }
        Map<String, Object> cacheMap = new HashMap<String, Object>();
        cacheMap.put(url, nowDataMap);
        redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
        return false;
    }

    /**
     * Determine whether the parameters are the same
     */
    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);
    }

    /**
     * Judging the interval between two times
     */
    private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval)
    {<!-- -->
        long time1 = (Long) nowMap. get(REPEAT_TIME);
        long time2 = (Long) preMap. get(REPEAT_TIME);
        if ((time1 - time2) < interval)
        {<!-- -->
            return true;
        }
        return false;
    }
}

Solve the problem of reading parameters: HttpServletRequest and RepeatedlyRequestWrapper

In the project, it often happens that the HTTP request body is read multiple times. At this time, an error may be reported. The reason is that the operation of reading the HTTP request body will eventually call the getInputStream() method and getReader() method of HttpServletRequest, and this The two methods can only be called once in total, and an error will be reported when the second call is made.

RepeatableFilter wraps HttpServletRequest with RepeatedlyRequestWrapper. Save the byte stream data of HttpServletRequest into a variable, rewrite the getInputStream() method and getReader() method, read the data from the variable, and return it to the caller.

public class RepeatableFilter implements Filter
{<!-- -->
    @Override
    public void init(FilterConfig filterConfig) throws ServletException
    {<!-- -->

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException
    {<!-- -->
        ServletRequest requestWrapper = null;
        if (request instanceof HttpServletRequest
                 & amp; & amp; StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE))
        {<!-- -->
        // Wrapped HttpServletRequest
            requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
        }
        if (null == requestWrapper)
        {<!-- -->
            chain.doFilter(request, response);
        }
        else
        {<!-- -->
            chain.doFilter(requestWrapper, response);
        }
    }

    @Override
    public void destroy()
    {<!-- -->

    }
}
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
{<!-- -->
    private final byte[] body;

    public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException
    {<!-- -->
        super(request);
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");

        body = HttpHelper.getBodyString(request).getBytes("UTF-8");
    }

    @Override
    public BufferedReader getReader() throws IOException
    {<!-- -->
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
// Rewritten, core: final ByteArrayInputStream bais = new ByteArrayInputStream(body);
    @Override
    public ServletInputStream getInputStream() throws IOException
    {<!-- -->
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
        return new ServletInputStream()
        {<!-- -->
            @Override
            public int read() throws IOException
            {<!-- -->
                return bais. read();
            }

            @Override
            public int available() throws IOException
            {<!-- -->
                return body. length;
            }

            @Override
            public boolean isFinished()
            {<!-- -->
                return false;
            }

            @Override
            public boolean isReady()
            {<!-- -->
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener)
            {<!-- -->

            }
        };
    }
}