防重复提交

使用场景

1. 针对前端页面

网络问题导致用户重复点击,发了多次请求引起的重复提交

2. 针对系统间接口调用

因网络超时重试或者程序bug导致的重复请求

3. 通用的业务层面的验证

比如要限制某个接口的一些参数的组合在规定的时间内只允许请求一次

实现方式

通过注解的方式,获取能标识重复请求的唯一的 key 作为 redis key,允许指定超时时间,在这段时间内,这种请求只允许通过一次。

获取唯一标识有三种方式,对应三种处理策略,只允许指定一个。三种方式分别为:

1、调用方自己传过来一个唯一的 id,放到请求的 header 中

2、能获取到登录用户的登录信息的情况下,通过 userId 和请求的 Uri 的组合生成唯一的 key

3、通过指定请求的参数的字段组合,通过这些字段组合生成唯一 key,请求字段通过 spEl 来指定。

使用场景示例

1. 前端表单提交的场景

解决方案:

1)使用策略1,基于唯一id的验证。

在打开一个提交页面的时候,前端可以生成一个全局唯一的 token 值,可自己生成或者调后端接口,请求后端提交接口的时候带上这个 token 值。

这样如果在第一次请求没有相应,用户快速点击的时候,因为在同一个页面,还没有刷新,所以该页面的所有请求携带的 token 值是相同的,这样只有第一次请求被正确处理。

2)使用策略2,基于用户的验证。这种对应前端不需要改动,后端自动获取用户信息和请求的URI。

2. 接口调用超时的场景

如果请求中参数有一个字段是唯一的,则可以使用策略1和3,如果是组合唯一则使用策略3

3. 通用的业务层面的接口参数的验证

使用策略3,通过spEl在注解中指定要验证的组合字段,举例:

ini 复制代码
@RepeatCheck(expireTime = 2000,
        keyExpression = "#index + '_' + #value + '_'+ #fundDTO.fundType + '_' + #fundDTO.fundChannel")

实战

接下来我们重点讲解业务层通用去重校验,实现思路:

  1. 注解式:利用 AOP 切面能力
  2. 业务层自定义唯一 Key:利用 spEL 表达式解析
  3. redis 存储校验

大致实现如下:

java 复制代码
    @Before("@annotation(annotation)")
    public Object repeatCheck(JoinPoint joinPoint, ResubmitCheck annotation) throws Throwable {
        Object[] args = joinPoint.getArgs();
        String[] conditionExpressions = annotation.conditionExpressions();
        if (ExpressionUtils.getConditionValue(args, conditionExpressions) || ArrayUtils.isEmpty(args)) {
            this.doCheck(annotation, args);
        }

        return true;
    }


    private void doCheck(@NonNull ResubmitCheck annotation, Object[] args) {
        if (annotation == null) {
            throw new NullPointerException("annotation cannot be null");
        }

        String keyExpressions = annotation.keyExpression();
        String message = annotation.message();
        boolean withUserInfoInKey = annotation.withUserInfoInKey();
        String methodDesc = this.request.getMethod();
        String uri = this.request.getRequestURI();

        StringBuilder bizKey = new StringBuilder(64);
        Object[] argsKeys = ExpressionUtils.getExpressionValue(args, keyExpressions);
        int length = argsKeys.length;

        for(int i = 0; i < length; ++i) {
            Object obj = argsKeys[i];
            bizKey.append(obj.toString());
        }

        StringBuilder keyBuilder = new StringBuilder();
        String userId = Objects.isNull(this.request.getHeader("uerId")) ? "0" : this.request.getHeader("userId");
        keyBuilder.append("repeatCheck::").append(withUserInfoInKey ? userId + "::" : "").append(uri).append("::").append(methodDesc).append("::").append(bizKey.toString());
        if (this.stringRedisTemplate.opsForValue().get(keyBuilder.toString()) != null) {
            throw new ResubmitException(10001, StringUtils.isBlank(message) ? "重复提交" : message);
        } else {
            this.stringRedisTemplate.opsForValue().set(keyBuilder.toString(), "", annotation.expireTime(), annotation.timeUnit());
        }
    }

注解定义:

java 复制代码
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RepeatCheck {
    String keyExpression();

    String message() default "重复提交";

    boolean withUserInfoInKey() default false;

    long expireTime() default 1L;

    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

如此,一个通用的防重组件大致完成了...

相关推荐
秦禹辰几秒前
宝塔面板安装MySQL数据库并通过内网穿透工具实现公网远程访问
开发语言·后端·golang
lypzcgf10 分钟前
Coze源码分析-资源库-删除插件-后端源码-应用和领域服务层
后端·go·coze·coze插件·coze源码分析·智能体平台·ai应用平台
敲上瘾10 分钟前
Docker 存储卷(Volume)核心概念、类型与操作指南
linux·服务器·数据库·docker·容器·架构
lssjzmn15 分钟前
Spring Web 异步响应实战:从 CompletableFuture 到 ResponseBodyEmitter 的全链路优化
java·前端·后端·springboot·异步·接口优化
shark_chili20 分钟前
程序员必知的底层原理:CPU缓存一致性与MESI协议详解
后端
John_ToDebug23 分钟前
从源码视角全面解析 Chrome UI 布局系统及 Views 框架的定制化实现方法与实践经验
c++·chrome·架构
一水鉴天31 分钟前
整体设计 之 绪 思维导图引擎 :思维价值链分层评估的 思维引导和提示词导航 之 引 认知系统 之8 之 序 认知元架构 之3(豆包助手 之5)
架构·认知科学
愿时间能学会宽恕36 分钟前
SpringBoot后端开发常用工具详细介绍——SpringSecurity认证用户保证安全
spring boot·后端·安全
CodeSheep1 小时前
稚晖君又开始摇人了,有点猛啊!
前端·后端·程序员
小宁爱Python1 小时前
Django 从环境搭建到第一个项目
后端·python·django