当前位置:   article > 正文

java防止重复提交(幂等性)问题_java 如何预防连续点击按钮而导致多次调用接口?

java 如何预防连续点击按钮而导致多次调用接口?

前言

前段时间测试提出了一个bug,因为前端没有做防抖,导致APP按钮可以无限次点击提交,后端请求一直在不断触发。

一. 幂等性的概念

在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数. 更复杂的操作幂等保证是利用唯一交易号(流水号)实现.

我的理解:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的或者是对一个接口的重复操作

二. 解决方案

解决方案有很多中,这里以java 代码实现,用redis存储,对接口限制访问次数与访问时间.
1.自定义接口

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Prevent {

    /**
     * 限制的时间值(秒)默认60s
     */
    long value() default 60;

    /**
     * 限制规定时间内访问次数,默认只能访问一次
     */
    long times() default 1;
    /**
     * 禁用时长
     *
     * @return
     */
    long forbiddenTime() default 60L;
    /**
     * 提示
     */
    String message() default "";

    /**
     * 策略
     */
    PreventStrategy strategy() default PreventStrategy.DEFAULT;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

2.实现切面编程

@Aspect
@Component
@Slf4j
public class PreventAop implements Serializable {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;


    /**
     * 切入点
     */
    @Pointcut("@annotation(com.health.boot.prevent.Prevent)")
    public void pointcut() {
    }


    /**
     * 处理前
     */
    @Before("pointcut()")
    public void joinPoint(JoinPoint joinPoint) throws Exception {
        // 获取调用者ip
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
        String userIP = IpUtils.getIpAddr(httpServletRequest);
        // 获取调用接口方法名
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = joinPoint.getTarget().getClass().getMethod(
                methodSignature.getName(),
                methodSignature.getParameterTypes()); // 获取该接口方法
        String methodFullName = method.getDeclaringClass().getName() + method.getName(); // 获取到方法名
        Prevent preventAnnotation = method.getAnnotation(Prevent.class); // 获取该接口上的prevent注解(为了使用该注解内的参数)
        // 执行对应策略
        entrance(preventAnnotation, userIP, methodFullName);
    }


    /**
     * 通过prevent注册判断执行策略
     *
     * @param prevent        该接口的prevent注解对象
     * @param userIP         访问该接口的用户ip
     * @param methodFullName 该接口方法名
     */
    private void entrance(Prevent prevent, String userIP, String methodFullName) throws Exception {
        PreventStrategy strategy = prevent.strategy(); // 获取校验策略
        if (Objects.requireNonNull(strategy) == PreventStrategy.DEFAULT) { // 默认就是default策略,执行default策略方法
            defaultHandle(userIP, prevent, methodFullName);
        } else {
            throw new MessageException("无效的策略");
        }
    }


    /**
     * Default测试执行方法
     *
     * @param userIP         访问该接口的用户ip
     * @param prevent        该接口的prevent注解对象
     * @param methodFullName 该接口方法名
     */
    private void defaultHandle(String userIP, Prevent prevent, String methodFullName) throws Exception {
        String loginUserId = SysUserUtil.getLoginUserId();
        String base64StrIP = toBase64String(userIP); // 加密用户ip(避免ip存在一些特殊字符作为redis的key不合法)
        long expire = prevent.value(); // 获取访问限制时间
        long times = prevent.times(); // 获取访问限制次数
        String redisKey =RedisKey.PREVENT_METHOD_NAME+ String.format("%s:%s:%s", base64StrIP,methodFullName,loginUserId);
        Object count = redisTemplate.opsForValue().get(redisKey);
        if(Objects.isNull(count)){
            log.info("首次访问");
            redisTemplate.opsForValue().set(redisKey,1,expire,TimeUnit.SECONDS);
        }else {
            if((Integer)count<times){
                log.info("总请求次数+1:{}", count);
                redisTemplate.opsForValue().increment(redisKey);
            }else {
                if((Integer)count>(times*3)){
                    //访问次数异常超出
                    redisTemplate.opsForValue().set(redisKey,1,prevent.forbiddenTime(),TimeUnit.SECONDS);
                    throw new MessageException("疑似违规操作,我们已经记录!");
                }
                log.info("请求频繁:{}", count);
                // 如果有限制信息则使用限制信息,没有则使用默认限制信息
                String errorMessage = prevent.message();
                if (StrUtil.isEmpty(errorMessage)) {
                    errorMessage =
                            !StrUtil.isEmpty(prevent.message()) ? prevent.message() : expire + "秒内不允许重复请求";
                }
                throw new MessageException(errorMessage);
            }
        }
    }


    public Long checkRequest(String key, Prevent prevent){
        StringBuffer redisScript=new StringBuffer();
        redisScript.append("if tonumber(redis.call('get', KEYS[1]) or '0') + 1 > tonumber(ARGV[1]) then ");
        redisScript.append(" return 0");
        redisScript.append("else ");
        redisScript.append(" value + 1 ");
        redisScript.append(" redis.call('INCRBY', KEYS[1], 1) ");
        redisScript.append(" redis.call('EXPIRE', KEYS[1], ARGV[2])  ");
        redisScript.append(" return 1");
        redisScript.append("end ");
        DefaultRedisScript<Long> defaultRedisScript= new DefaultRedisScript<>();
        defaultRedisScript.setResultType(Long.class);
        defaultRedisScript.setScriptText(redisScript.toString());
        Long execute = redisTemplate.execute(defaultRedisScript, Collections.singletonList(key), prevent.value(), prevent.times());
        return execute;
    };


    /**
     * 对象转换为base64字符串
     *
     * @param obj 对象值
     * @return base64字符串
     */
    private String toBase64String(String obj) throws Exception {
        if (StrUtil.isEmpty(obj)) {
            return null;
        }
        Base64.Encoder encoder = Base64.getEncoder();
        byte[] bytes = obj.getBytes(StandardCharsets.UTF_8);
        return encoder.encodeToString(bytes);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  1. 设置默认响应时间
@Getter
public enum PreventStrategy {
    /**
     * 默认(60s内不允许再次请求)
     */
    DEFAULT
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  1. 实现方式
    @PostMapping(value = "saveUserAnswer")
    @Prevent(value = 5, times = 1, message = "请勿重复提交,休息一下吧!")
    public String saveUserAnswer(@Valid @RequestBody ProblemLibraryUserAnswer problemLibraryUserAnswer, HttpServletRequest request) {
        final String id = UserInfo.id(request, sign);
        problemLibraryUserAnswer.setUserId(id);
        return problemLibraryService.saveUserAnswer(problemLibraryUserAnswer) ? ResultUtil.success(ResultEnum.SUCCESS, "答题成功")
                : ResultUtil.error(ResultEnum.MSG_NOT_FOUND, "请重新答题");
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这里表示每5秒请求一次

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/码创造者/article/detail/889099
推荐阅读
相关标签
  

闽ICP备14008679号