解决表单重复提交问题

防止表单重复提交

一、问题背景

​ 现管理平台退货功能,支持上传退货excel并往退货订单表插入退货订单记录功能。运营同事反馈出现上传退货excel时,退货订单表出现重复退货记录,即存在表单重复提交的问题。

​ 由于历史原因,该退货订单表refundOrderId非唯一索引,无法从数据库层面解决该问题,只能从前后端代码解决表单重复提交的问题。

二、解决方案

避免表单重复提交问题,前端表单提交时,可以在点击表单提交按钮后将按钮置灰,也就是禁止用户再次点击,实现方案如下:


注意:禁用表单提交按钮只能在前端防范用户表单重复提交,无法防止恶意通过接口的方式发起重复提交,更安安全的方案也就是token方案来解决该问题。

以下介绍token实现避免表单重复提交的方案:

  • 进入表单页面前,后台先生成token并存至缓存,注意该token全局唯一

  • 表单提交时带上该token

  • 后台校验缓存是否存在token

    1、若存在则说明token合法,非重复提交,放行并从缓存中删除该token;

    2、若缓存不存在该token则说明系非法表单提交或者重复表单提交,拒绝。

具体实现时,可以针对生成token、校验token实现一个专门的@SubmitToken注解,代码如下:

/**
 * 提交token
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SubmitToken {
    /**
     * 创建token
     * @return
     */
    boolean create() default false;

    /**
     * 校验token
     * @return
     */
    boolean check() default false;
}

针对该token注解,基于AOP拦截该注解并生成或校验token,代码如下:

/**
 * 拦截重复表单提交请求
 */
@Slf4j
@Aspect
@Service
public class NoRepeatSubmitAop {

    @Autowired
    private GeneralNearCache generalNearCache;

    public NoRepeatSubmitAop() {
        log.info("防止重复表单提交切面启动");
    }

    /**
     * 定义切点
     */
    @Pointcut("@annotation(com.chinaums.xxx.web.mgrframework.annotation.SubmitToken)")
    public void repeatSubmitPtCut() {

    }

    /**
     * 环绕通知处理
     */
    @Around("repeatSubmitPtCut()")
    public Object checkRepeatSubmit(ProceedingJoinPoint pjp) throws Throwable {

        HttpServletRequest request = null;
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            request = attributes.getRequest();
        }

        if(request != null) {

            MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
            SubmitToken submitTokenAnnotation = methodSignature.getMethod().getAnnotation(SubmitToken.class);

            if(submitTokenAnnotation.create()) {
                String token = UUIDGenerator.getUUID();
                request.setAttribute(WebConstant.SUBMIT_TOKEN, token);
                generalNearCache.putToCache(token, token);
            } else if(submitTokenAnnotation.check()) {

                // 直接放行get请求
                if(StringUtils.equals(request.getMethod(), RequestMethod.GET.toString())) {
                    return pjp.proceed();
                }

                String reqToken = request.getParameter(WebConstant.SUBMIT_TOKEN);
                String token = generalNearCache.getFromCache(reqToken);

                if(StringUtils.isNotBlank(reqToken) && StringUtils.equals(reqToken, token)) {
                    generalNearCache.removeFromCache(reqToken);
                    return pjp.proceed();
                }

                throw new RepeatFormSubmitException(ResultBean.REPEAT_SUBMIT_FAIL, "重复表单请求");
            }
        }

        return pjp.proceed();
    }
}

在跳转到表单提交页面之前,后端controller加上@SubmitToken(create = true)生成token注解:

@SubmitToken(create = true)
@RequestMapping(value = "/goSubmitForm.do", method = RequestMethod.GET)
public String goSubmitForm() {
    return "submitForm";
}

页面表单提交时带上token


对应的js代码:

	function formSubmit(e){
		var action = document.form.action + "?submit_token="+$("#submit_token").val();
		document.form.action=action;
		document.form.submit();
		e.disabled=true;
	}

后端controller加上@SubmitToken(check=true)校验token注解:


    @RequestMapping(value = "/submitForm.do", method = RequestMethod.POST)
    @OperateLog(operType = DataConstant.OPER_TYPE_APPROVE)
    @SubmitToken(check = true)
    public void submitForm(MultipartFile uploadFile, HttpServletRequest request,
                                     HttpServletResponse response) throws Exception {

    }

你可能感兴趣的:(JavaWeb,java,前端,开发语言)