SpringBoot+Redis防止表单重复提交

@TOC

接口压力测试

SpringBoot+Redis防止表单重复提交_第1张图片
SpringBoot+Redis防止表单重复提交_第2张图片

SpringBoot+Redis防止表单重复提交_第3张图片
大家可以看到效果非常好,一秒钟一千次请求都是拦截住了,即使报错也是客户端Socket关闭

什么是表单重复提交

   各位大哥好,首先介绍一下什么是表单重复提交. (原谅我中英文句点不分,是因为开发的时候把中文的符号也切换成
   了英文的)一个场景就是小弟我在实习的时候做的第一个前后端分离的RESTFul接口,说白了也就是普通的增删改
   查,项目验收的时候,牛逼的部门经理小方发现了一个bug,删除一个实体(实体是轮播图,属性有id,图片url地
   址,文字,创建日期等)的时候报错了;

找出恶人

   然后我就顺着情况准备去复现一下,然后发现实在复现不了,百般无奈请教了我的师傅华佗.听说他老人家是华为出来
   的...卧槽说多了,师傅带我一阵排查,从本地debug到具体执行的sql日志再到前端分析,最后一天的时间师傅说
   实在找不出来你就乱点点,我一听也就放弃了,重点来了!!!!就在我随便点击的时候,一直疯狂点击添加按钮,终
   于发现了bug所在,添加按钮点击很快的时候,往数据库插入了两条一模一样的数据,如下图

显形吧 恶人

SpringBoot+Redis防止表单重复提交_第4张图片
大家会发现这两条记录在我疯狂手速的点击下竟然是除了主键ID外是一模一样的,然后最骚的Bug来了,点击第一张
轮播图删除键的时候,会根据附件图片的ID地址去另一张附件表删除,用的是delete from xxx
where imageId =xxx,这个时候是不会报错的然后你刷新列表,就会发现第二张图没了,为什么呢,因为它已经
不存在附件表里了,就是这么骚的操作.扯了一大圈,现在附上会出现表单重复提交的原因;
1.提交完表单以后,不做其他操作,直接刷新页面,表单会提交多次。
2.在提交表单时,如果网速较差,可能会导致点击提交按钮多次,这种情况也会导致表单重复提交。
3.单提交成功以后,直接点击浏览器上回退按钮,不刷新页面,然后点击提交按钮再次提交表单。

正题

1.怎么解决

怎么解决表单重复提交?首先可以前端js在提交按钮点击的同时设置按钮不可点击,当然这个可以被
绕过,咱们还是重点说一下后端怎么做的吧.
场景:添加用户,添加框打开的同时,请求后端一个接口,返回token(只要是加密唯一串即可),存入redis,前端拿到token后,在提交按钮点击的同时,重写请求头,把token加入的header中,然后后端拿到token,去和redis当中的值做对比,如果二者相等,则redis.delete(token);如果redis当中没有值,就证明已经被delete了,即表单已经提交一次,不允许重复提交;如果二者不相等,很明显,伪造token,直接返回

2.实现方法

2.1构造TokenUtil

package com.qdu.niit.util.token;

import com.qdu.niit.config.redisconfig.RedisKey;
import com.qdu.niit.core.Result;
import com.qdu.niit.core.ResultGenerator;
import com.qdu.niit.util.StringUtil;
import com.qdu.niit.util.redis.RedisUtil;
import com.xiaoleilu.hutool.util.RandomUtil;
import com.xiaoleilu.hutool.util.StrUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * @author [email protected]
 * @version 0.5
 * @date Created in 2020-04-13 17:17
 * @description 描述
 * @modified By
 */
@Component
public class TokenUtil {

    @Autowired
    private RedisUtil redisUtil;

    public String createToken() {
        String token = RandomUtil.randomUUID();
        String tokenKey = RedisKey.getToken(token);
        redisUtil.set(tokenKey, token, 60 * 10L);
        return token;
    }

    public Result checkToken(HttpServletRequest request) throws Exception {

        String token = request.getHeader("Authorization");
        // header中不存在token
        if (StrUtil.isBlank(token)) {
            token = request.getParameter("Authorization");
            // parameter中也不存在token
            if (StrUtil.isBlank(token)) {
                return ResultGenerator.genFailResult("未传token");
            }
        }

        String tokenKey = RedisKey.getToken(token);
        String systemToken = (String) redisUtil.get(tokenKey);
        if (StringUtil.isEmpty(systemToken)) {
            return ResultGenerator.genFailResult("请不要重复提交");
        }

        if (!systemToken.equals(token)) {
            return ResultGenerator.genFailResult("无效token");
        }
        redisUtil.remove(tokenKey);
        return ResultGenerator.genSuccessResult("");
    }
}

2.2构造自定义主键 @AutoIdempotent

package com.qdu.niit.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author hucs
 * @date 2020/4/13 17:58
 * @since JDK 1.8
 */
//方法级注解
@Target(ElementType.METHOD)
//运行时有效
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}

2.3自定义拦截器AutoIdempotentInterceptor

package com.qdu.niit.config;

import com.alibaba.fastjson.JSON;
import com.qdu.niit.annotation.AutoIdempotent;
import com.qdu.niit.core.Result;
import com.qdu.niit.exception.CustomException;
import com.qdu.niit.util.token.TokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;

/**
 * @author [email protected]
 * @version 0.5
 * @date Created in 2020-04-13 18:34
 * @description 描述
 * @modified By
 */
@Component
@Slf4j
public class AutoIdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenUtil tokenUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        //被ApiIdempotment标记的扫描
        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
        if (methodAnnotation != null) {

            Result result = tokenUtil.checkToken(request);
            if (result.getCode().equals("0000")) {
                return true;
            } else {
                throw new CustomException(7777, result.getMessage());
            }
        }
        return true;
    }
    //必须返回true,否则会被拦截一切请求

    private void responseResult(HttpServletResponse response, Result result) {
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-type", "application/json;charset=UTF-8");
        response.setStatus(200);
        try {
            response.getWriter().write(JSON.toJSONString(result));
        } catch (IOException ex) {
            log.error(ex.getMessage());
        }
    }

    public boolean isAjaxRequest(HttpServletRequest request) {
        String requestType = request.getHeader("X-Requested-With");
        //如果requestType能拿到值,并且值为 XMLHttpRequest ,表示客户端的请求为异步请求,那自然是ajax请求了,反之如果为null,则是普通的请求
        if (requestType == null) {
            return false;
        }
        return true;

    }
}

2.4将拦截器配置到SpringBoot

package com.qdu.niit.config;

import com.alibaba.fastjson.JSON;
import com.qdu.niit.annotation.AutoIdempotent;
import com.qdu.niit.core.Result;
import com.qdu.niit.exception.CustomException;
import com.qdu.niit.util.token.TokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;

/**
 * @author [email protected]
 * @version 0.5
 * @date Created in 2020-04-13 18:34
 * @description 描述
 * @modified By
 */
@Component
@Slf4j
public class AutoIdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenUtil tokenUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        //被ApiIdempotment标记的扫描
        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
        if (methodAnnotation != null) {

            Result result = tokenUtil.checkToken(request);
            if (result.getCode().equals("0000")) {
                return true;
            } else {
                throw new CustomException(7777, result.getMessage());
            }
        }
        return true;
    }
    //必须返回true,否则会被拦截一切请求

    private void responseResult(HttpServletResponse response, Result result) {
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-type", "application/json;charset=UTF-8");
        response.setStatus(200);
        try {
            response.getWriter().write(JSON.toJSONString(result));
        } catch (IOException ex) {
            log.error(ex.getMessage());
        }
    }

    public boolean isAjaxRequest(HttpServletRequest request) {
        String requestType = request.getHeader("X-Requested-With");
        //如果requestType能拿到值,并且值为 XMLHttpRequest ,表示客户端的请求为异步请求,那自然是ajax请求了,反之如果为null,则是普通的请求
        if (requestType == null) {
            return false;
        }
        return true;

    }
}

2.5前端

							//拿到token
                            $.ajax({
                                url: '/get/token',
                                type: 'get',
                                dataType: "json",
                                success: function (res) {
                                    if (res.code == 0000) {
                                    //将拿到的token赋值隐藏域,其实没什么必要,定义变量存储更好
                                        $('#addToken').val(res.data);
                                    } else {
                                        layer.msg(res.message);
                                    }
                                }
                            });

//
           //提交表单
            $.ajax({
                url: '/console/user',
                type: 'post',
                data: JSON.stringify(data.field),
                beforeSend: function (request) {
                    request.setRequestHeader("Authorization", $('#addToken').val());
                },
                dataType: "json",
                contentType: 'application/json;charset=utf-8',
                success: function (res) {
                    if (res.code == 0000) {
                        layer.msg('添加成功');
                        //清空表单
                        $('#addForm')[0].reset();
                        layer.close(divindex);
                        $('.layui-laypage-btn').click();
                    } else {
                        layer.msg(res.message);
                    }
                }
            });

测试

token

SpringBoot+Redis防止表单重复提交_第5张图片

提交表单

表单第一次提交

SpringBoot+Redis防止表单重复提交_第6张图片

SpringBoot+Redis防止表单重复提交_第7张图片
OK 大功告成

你可能感兴趣的:(SpringBoot技术栈)