天天看點

使用攔截器防止表單重複送出

作者:頑石九變

業務場景介紹

web系統經常會出現使用者在頁面上快速點選多次送出按鈕(或者重複重新整理頁面),在背景會連續接收多次請求,除了第一次外,其他的相同請求就是重複送出。

如何避免頁面重複送出呢,正常有以下幾種方法:

  1. 前台控制:點選後,使用js将按鈕事件移除
  2. 背景控制:生成token存儲session中,使用頁面攔截器校驗
  3. 背景控制:生成token放入redis中并設定有效期,讓其自動失效

方法1過于簡單暴力,有一定效果;方法2有點複雜也不是很靈活,其實和方法2有點類似

這裡要詳細介紹的是方法2,通過攔截器+注解,使用簡單靈活。

使用頁面攔截器校驗token,防止重複送出

基本原理

  1. 通過攔截器攔截頁面請求
  2. 在打開頁面時生成token,并存儲在session中
  3. 頁面上送出時将token傳到背景,在攔截器中校驗token
  4. 如果token不比對則拒絕請求,并傳回錯誤資訊
  5. 如果token比對,則删除或重新生成token

詳細代碼

1、添加springMvc請求攔截器AvoidDuplicateSubmitInterceptor.java

/**
 * 頁面重複送出攔截器
 * 
 * @author huangjian
 *
 */
public class AvoidDuplicateSubmitInterceptor extends HandlerInterceptorAdapter {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            SubmitToken annotation = method.getAnnotation(SubmitToken.class);
            if (annotation == null) {
                return true;
            }
            HttpSession session = request.getSession(false);
            if (annotation.value().equals(TokenType.create)) {
                SessionTokenUtil.createToken(session);
                return true;
            }
            if (isRepeatSubmit(request)) {
                logger.warn("頁面重複送出,url:{}", request.getRequestURI());
                RenderUtil.renderFailure("頁面重複送出", response);
                return false;
            }
            if (annotation.value().equals(TokenType.refresh)) {
                // 驗證成功後,重新生成token
                SessionTokenUtil.createToken(session);
            } else {
                // 驗證成功後,删除token
                SessionTokenUtil.removeToken(session);
            }
        }
        return true;
    }

    /** 判斷是否重複送出 */
    private boolean isRepeatSubmit(HttpServletRequest request) {
        String serverToken = SessionTokenUtil.getToken(request.getSession(false));
        if (serverToken == null) {
            return true;
        }
        String clientToken = request.getParameter(SessionTokenUtil.TOKEN_KEY);
        if (clientToken == null) {
            return true;
        }
        if (!serverToken.equals(clientToken)) {
            return true;
        }
        return false;
    }
}
           

2、添加工具類SessionTokenUtil.java

public class SessionTokenUtil {
    public static final String TOKEN_KEY = "submitToken";

    /** 生成10位随機數作為token */
    private static String createToken() {
        return RandomUtils.generateString(10);
    }

    public static void createToken(HttpSession session) {
        session.setAttribute(SessionTokenUtil.TOKEN_KEY, createToken());
    }

    public static void removeToken(HttpSession session) {
        session.removeAttribute(SessionTokenUtil.TOKEN_KEY);
    }

    public static String getToken(HttpSession session) {
        String serverToken = (String) session.getAttribute(TOKEN_KEY);
        return serverToken;
    }
}
           

3、添加注解SubmitToken.java,以及枚舉TokenType.java

@Retention(RetentionPolicy.RUNTIME)
@Target({ METHOD })
public @interface SubmitToken {

    TokenType value() default TokenType.create;
}
           
public enum TokenType {
    create, remove, refresh
}
           

4、在controller中使用

1)在打開頁面方法标記生成token(方法僅是示例)

/** 進入子產品 詳細/新增 頁 */
@SubmitToken(TokenType.create)
@RequestMapping(value = "detail/{id}", method = RequestMethod.GET)
public String detail(@PathVariable("id") Long id, Model model, HttpServletRequest request) {

}
           

2)在送出儲存方法标記重新整理或删除token(方法僅是示例)

@SubmitToken(TokenType.refresh)
@RequestMapping(value = "save", method = RequestMethod.POST)
public void save(@Valid @ModelAttribute("preload") BsCx entity, HttpServletRequest request,
        HttpServletResponse response) {
    try {
        //TODO 執行儲存操作...
        // 将新的token傳回給頁面,并在頁面重新整理token
        String serverToken = SessionTokenUtil.getToken(request.getSession(false));
        RenderUtil.renderSuccess(serverToken, response);
    } catch (Exception e) {
        //
    }
}
           

3)在頁面上面添加token值,如果需要支援連續操作,要在儲存成功後,更新submitToken值

<input type="hidden" id="submitToken" name="submitToken" th:value="${session.submitToken}" />
           
上面是thymeleaf模闆示例代碼

5、配置攔截器

下面是springboot配置示例

@Configuration
public class WebAppConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AvoidDuplicateSubmitInterceptor());
    }

}