Recently, while developing tests locally, I encountered a duplicate form submission. I hit the submit button twice due to network latency, and two records were generated in the database. In fact, this phenomenon has been encountered before, generally after the submission of the button ash, unable to submit again, this is a very common way to deal with the client. But this is not a fundamental solution to the problem, although the client solved the problem of multiple submissions, there are still problems in the interface. Let’s say we’re not submitting from the client, we’re being called by some other system call, and we’re going to have this problem when the system compensates for the network latency

1. Pass the token authentication in the session

  1. When the page is initialized, a unique token is generated and placed in the page hidden field and session
  2. The interceptor intercepts the request and verifies that the token from the page request is the same as the token in the session
  3. If yes, the submission is successful and the token in the session is removed. If no, the submission is repeated and logs are recorded

Step 1: Create custom annotations

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
    boolean save(a) default false;
    boolean remove(a) default false;
}
Copy the code

Step 2: Create a custom interceptor (@slf4j is lombok’s annotation)

@Slf4j
public class RepeatSubmitInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        HandlerMethod handlerMethod = null;
        try {
            handlerMethod = (HandlerMethod)handler;
        } catch (Exception e) {
            return true;
        }
        Method method = handlerMethod.getMethod();

        Token token = method.getAnnotation(Token.class);
        if(token ! =null) {boolean saveSession = token.save();
            if(saveSession){
                request.getSession(true).setAttribute("token", UUID.randomUUID());
            }

            boolean removeSession = token.remove();
            if(removeSession){
                if(isRepeatSubmitSession(request)){
                    log.info("repeat submit session :" + request.getServletPath());
                    response.sendRedirect("/error/409");
                    return false;
                }
                request.getSession(true).removeAttribute("token"); }}return true;
    }

    private boolean isRepeatSubmitSession(HttpServletRequest request){
        String sessionToken = String.valueOf(request.getSession(true).getAttribute("token") = =null ? "" : request.getSession(true).getAttribute("token"));
        String clientToken =  String.valueOf(request.getParameter("token") = =null ? "" : request.getParameter("token"));
        if(sessionToken == null || sessionToken.equals("")) {return true;
        }
        if(clientToken == null || clientToken.equals("")) {return true;
        }
        if(! sessionToken.equals(clientToken)){return true;
        }
        return false; }}Copy the code

Step 3: Add a custom interceptor to the configuration file

<mvc:interceptor>
	<mvc:mapping path="/ * *"/>
	<bean class="com.chinagdn.base.common.interceptor.RepeatSubmitInterceptor"/>
</mvc:interceptor>
Copy the code

Use case

	// Save = true is used to generate tokens
	@Token(save = true)
    @RequestMapping(value = "save", method = RequestMethod.GET)
    public String save(LoginUser loginUser, Model model) throws Exception {
        return "sys/user/edit";
    }
	//remove = true to verify the token
	@Token(remove = true)
    @RequestMapping(value = "save", method = RequestMethod.POST)
    public String save(@Valid LoginUser loginUser, Errors errors, RedirectAttributes redirectAttributes, Model model) throws Exception {
    	/ /...
    }
Copy the code

JSP page hidden domain add token

	<input type="hidden" name="token" value="${sessionScope.token}">
Copy the code

2. Verify the repeated submission by the url and parameters last requested by the current user

  1. The interceptor intercepts the request and compares the URL and parameters of the previous request to this one
  2. Check whether they are consistent. The commit is repeated and logs are recorded

Step 1: Create custom annotations

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SameUrlData {

}
Copy the code

Step 2: Create a custom interceptor

public class SameUrlDataInterceptor 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();
            // Whether there is a SameUrlData annotation
            SameUrlData annotation = method.getAnnotation(SameUrlData.class);
            if(annotation ! =null) {
                if (repeatDataValidator(request)) {// If the same data is repeated
                    response.sendRedirect("/error/409");
                    return false;
                } else {
                    return true; }}return true;
        } else {
            return super.preHandle(request, response, handler); }}/** * verify that the same URL is submitted, and return true *@param httpServletRequest
     * @return* /
    private boolean repeatDataValidator(HttpServletRequest httpServletRequest) {
        String params = JsonMapper.toJsonString(httpServletRequest.getParameterMap());
        String url = httpServletRequest.getRequestURI();
        Map<String, String> map = new HashMap<>();
        map.put(url, params);
        String nowUrlParams = map.toString();//

        Object preUrlParams = httpServletRequest.getSession().getAttribute("repeatData");
        if (preUrlParams == null) { // If the previous data is null, the page has not been accessed
            httpServletRequest.getSession().setAttribute("repeatData", nowUrlParams);
            return false;
        } else { // Otherwise, the page has already been visited
            if (preUrlParams.toString().equals(nowUrlParams)) { // If the last URL + data is the same as this url+ data, then the data is added
                return true;
            } else { // If the last URL + data is different from this url+ data, it is not a double submission
                httpServletRequest.getSession().setAttribute("repeatData", nowUrlParams);
                return false; }}}}Copy the code

Step 3: Add a custom interceptor to the configuration file

<mvc:interceptor>
	<mvc:mapping path="/ * *"/>
	<bean class="com.chinagdn.base.common.interceptor.SameUrlDataInterceptor"/>
</mvc:interceptor>
Copy the code

Use case

	// Use the @sameurlData annotation on the Controller layer
	@SameUrlData
    @RequestMapping(value = "save", method = RequestMethod.POST)
    public String save(@Valid LoginUser loginUser, Errors errors, RedirectAttributes redirectAttributes, Model model) throws Exception {
    	/ /...
    }
Copy the code