天天看点

使用拦截器防止表单重复提交

业务场景介绍

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());
    }

}