業務場景介紹
web系統經常會出現使用者在頁面上快速點選多次送出按鈕(或者重複重新整理頁面),在背景會連續接收多次請求,除了第一次外,其他的相同請求就是重複送出。
如何避免頁面重複送出呢,正常有以下幾種方法:
- 前台控制:點選後,使用js将按鈕事件移除
- 背景控制:生成token存儲session中,使用頁面攔截器校驗
- 背景控制:生成token放入redis中并設定有效期,讓其自動失效
方法1過于簡單暴力,有一定效果;方法2有點複雜也不是很靈活,其實和方法2有點類似
這裡要詳細介紹的是方法2,通過攔截器+注解,使用簡單靈活。
使用頁面攔截器校驗token,防止重複送出
基本原理
- 通過攔截器攔截頁面請求
- 在打開頁面時生成token,并存儲在session中
- 頁面上送出時将token傳到背景,在攔截器中校驗token
- 如果token不比對則拒絕請求,并傳回錯誤資訊
- 如果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());
}
}