业务场景介绍
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());
}
}