[20][04][21] 表单重复提交

1. 业务背景

在日常业务中经常会出现短时间内重复点击提交按钮场景,例如抢票,电商秒杀,各种投票,薅羊毛活动

如果不做表单重复提交过滤,服务器压力很大,还有就是会存在安全问题,导致网站被薅羊毛

2. 表单重复提交解决方案

2.1 前端解决方案

提交后按钮禁用,置灰,页面出现遮罩

2.2 后端解决方案

使用表单提交专属 token,每个 token 只能使用一次

  • 在表单提交页面调用接口,获取此次表单提交需要使用的 token,后端将 token 存放至 redis 设置短时间过期策略
  • 在提交表单时,将生成的 token 放入 request 的 header 或者 body
  • 后端在收到表单提交请求后,从 header 或 body 中获取 token,如果能够从 redis 中获得该令牌 (获取后将当前令牌删除),则继续执行访问的业务逻辑,如果从 redis 获取不到则返回重复提交
前端 后端 redis 获取当前表单提交的token 生成token保存至redis 返回token 提交表单,token在header中 校验token是否在redis存在 token存在 删除token token有效,表单提交成功 再次提交表单,token在header中 校验token是否在redis存在 token不存在 重复提交表单 前端 后端 redis

2.2.1 注解

/**
 * 防止重复提交注解
 * @author oscar
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiRepeatSubmit {
    ConstantUtils value();
}

/**
 * 生成token注解
 * @author oscar
 */
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiToken {

}

2.2.2 枚举类

/**
 * 定义从哪里取Token的枚举类
 * @author oscar
 */
public enum ConstantUtils {
    /**
     * 从请求体中取token
     */
    BOOD,
    /**
     * 从请求头中取token
     */
    HEAD
}

/**
 * 响应码枚举类
 * @author oscar
 */
public enum ResponseCode {
    SUCCESS("000000", "成功"),
    // 通用模块 1xxxx
    ILLEGAL_ARGUMENT("100000", "参数不合法"),
    REPETITIVE_OPERATION("100001", "请勿重复操作"),
    ;

    ResponseCode(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    private String code;
    private String msg;

    public String getCode() {
        return code;
    }
    public void setCode(String code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
}

2.2.3 拦截器

/**
 * 重复提交表单拦截器
 * @author oscar
 */
@Aspect
@Component
@Slf4j
public class NoRepeatSubmitAop {

    /**
     * 表单提交 token 字段名
     */
    private static final String FORM_TOKEN_NAME = "formToken";

    @Autowired
    private RedisTokenUtils redisTokenUtils;

    /**
     * 将token放入请求
     *
     * @param pjp
     * @param nrs
     */
    @Before("execution(* com.zhunongyun.toalibaba.commoncode.controller.*Controller.*(..)) && @annotation(nrs)")
    public void before(JoinPoint pjp, ApiToken nrs) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletResponse response = attributes.getResponse();
        response.setHeader(FORM_TOKEN_NAME, redisTokenUtils.getToken());
    }


    /**
     * 拦截带有重复请求的注解的方法
     *
     * @param pjp
     * @param nrs
     */
    @Around("execution(* com.zhunongyun.toalibaba.commoncode.controller.*Controller.*(..)) && @annotation(nrs)")
    public Object arround(ProceedingJoinPoint pjp, ApiRepeatSubmit nrs) {

        try {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();

            String token = null;
            if (nrs.value() == ConstantUtils.BOOD) {
                //从 参数 中取Token
                token = (String) request.getAttribute(FORM_TOKEN_NAME);
            } else if (nrs.value() == ConstantUtils.HEAD) {
                //从 请求头 中取Token
                token = request.getHeader(FORM_TOKEN_NAME);
            }

            if (StringUtils.isEmpty(token)) {
                log.error("获取token失败");
                return ResponseVO.fail(ResponseCode.REPETITIVE_OPERATION, "获取token失败");
            }
            if (!redisTokenUtils.findToken(token)) {
                log.error("重复提交");
                return ResponseVO.fail(ResponseCode.REPETITIVE_OPERATION, "重复提交");
            }
            log.error("正常提交表单");
            Object o = pjp.proceed();
            return o;
        } catch (Throwable e) {
            log.error("验证重复提交时出现未知异常:{}", e);
            return ResponseVO.fail(ResponseCode.REPETITIVE_OPERATION, "验证重复提交时出现未知异常");
        }
    }
}

2.2.4 返回响应

@Data
public class ResponseVO {
    private String code;
    private String msg;
    private Object data;

    private ResponseVO() {

    }

    private ResponseVO(String code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    private ResponseVO(String code, String msg) {
        new ResponseVO(code, msg, null);
    }

    public static ResponseVO success() {
        return new ResponseVO(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg());
    }

    public static ResponseVO success(Object data) {
        return new ResponseVO(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg(), data);
    }

    public static ResponseVO fail(ResponseCode responseCode, Object data) {
        return new ResponseVO(responseCode.getCode(), responseCode.getMsg(), data);
    }

    public static ResponseVO fail(String code, String msg, Object data) {
        return new ResponseVO(code, msg, data);
    }

    public static ResponseVO fail(String code, String msg) {
        return fail(code, msg, null);
    }
}

2.2.5 接口

/**
 * 解决表单重复提交
 * @author oscar
 */
@RestController
@RequestMapping("form")
public class FormRepeatSubmissionController {

    /**
     * 进入页面
     * @return
     */
    @ApiToken
    @GetMapping("index")
    public ResponseVO index(){
        return ResponseVO.success();
    }

    /**
     * 提交表单
     * @param data
     * @return
     */
    @ApiRepeatSubmit(ConstantUtils.HEAD)
    @PostMapping("add")
    public ResponseVO saveData(@RequestBody String data) {
        return ResponseVO.success(data);
    }
}

2.2.6 测试

获取表单提交需要的 token

xx

第一次提交表单

xx

再次提交表单

xx

使用 JMeter 压测结果正常

xx

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值