• springboot接口防刷实现


    本文主要介绍一种通过实现自定义注解,实现一种比较通用的接口防刷方式

    前言

    1.基本准备

    1. jdk 8
    2. redis
    3. springboot 2.7.6

    2.基本思路

    主要就是借助 redis 来实现接口的防刷。

    基本逻辑:定义一个切面,通过@Prevent注解作为切入点、在该切面的前置通知获取该方法的所有入参;
    同时,通过@Prevent注解的convert属性,自定义redis的部分key值,并将其Base64编码+完整方法名作为redis的key,
    自定义redis的部分key值作为reids的value,@Prevent的time作为redis的expire,存入redis;

    每次进来这个切面根据自定义入参Base64编码+完整方法名判断redis值是否存在,存在则拦截防刷,不存在则允许调用;

    代码实现

    1.定义注解Prevent

    package com.terrytian.springboottq.annotation;
    
    import com.terrytian.springboottq.convert.PreventConvert;
    import com.terrytian.springboottq.handler.PreventHandler;
    
    import java.lang.annotation.*;
    
    /**
     *接口防刷注解
     *大致逻辑:
     *定义一个切面,通过@Prevent注解作为切入点、
     *在该切面的前置通知获取该方法的所有入参并自定义redis的部分key,* 将自定义redis的部分key的Base64编码+完整方法名作为redis的key,
     *自定义redis的部分ey作为reids的alue,@Prevent的vaLue作为redis的expire,存入redis;
     * 

    *使用: * 1.在相应需要防刷的方法上加上该注解,即可 * 2.接口有入参,无参的需要自定义covert * * @author: tianqing * @date:2022/11/26 */ @Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Prevent { /** * 限制的时间值(秒) * * @return */ String time() default "60"; /** * 提示 */ String message() default ""; /** * 是否支持用在空入参的方法上,自定义转换器后可以支持 * @return */ Class nullAble() default PreventConvert.class; /** * 转换器:用于定义redis的key * @return */ Class convert() default PreventConvert.class; /** * 处理策略 * @return */ Class strategy() default PreventHandler.class; }

    • 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

    2. 实现PreventAop

    package com.terrytian.springboottq.aop;
    
    import com.alibaba.fastjson.JSON;
    import com.terrytian.springboottq.annotation.Prevent;
    import com.terrytian.springboottq.common.BusinessCode;
    import com.terrytian.springboottq.common.BusinessException;
    import com.terrytian.springboottq.convert.PreventConvert;
    import com.terrytian.springboottq.handler.PreventHandler;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    
    import java.lang.reflect.Method;
    
    /**
     * 防刷切面实现类
     *
     * @author: tianqing
     * @date: 2022/11/26 20:27
     */
    @Aspect
    @Component
    @Slf4j
    public class PreventAop {
    
        /**
         * 切入点
         */
        @Pointcut("@annotation(com.terrytian.springboottq.annotation.Prevent)")
        public void pointcut() {
        }
    
    
        /**
         * 处理前
         *
         * @return
         */
        @Before("pointcut()")
        public void joinPoint(JoinPoint joinPoint) throws Exception {
            Object[] args = joinPoint.getArgs();
    
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();
    
            Prevent preventAnnotation = method.getAnnotation(Prevent.class);
            String methodFullName = method.getDeclaringClass().getName() + method.getName();
    
            //空入参方法处理逻辑
            Class convertNullAble = preventAnnotation.nullAble();
            if (convertNullAble.equals(PreventConvert.class)){
                String requestStr = JSON.toJSONString(joinPoint.getArgs()[0]);
                if (!StringUtils.hasText(requestStr) || requestStr.equalsIgnoreCase("{}")) {
                    throw new BusinessException("[防刷]入参不允许为空");
                }
            }else {
                //如果是A.isAssignableFrom(B) 确定一个类(B)是不是继承来自于另一个父类(A),一个接口(A)是不是实现了另外一个接口(B),或者两个类相同。
                if (PreventConvert.class.isAssignableFrom(convertNullAble)){
                    //允许用在空方法上,需要自定义
                    PreventConvert convert = convertNullAble.newInstance();
                    try {
                        convert.convert(args);
                    }catch (Throwable t){
                        log.error("[PreventAop]some errors happens in PreventAop's nullAble",t);
                    }
                }
            }
    
            StringBuilder sb = new StringBuilder();
    
            Class convertClazz = preventAnnotation.convert();
            //处理自定义convert
            boolean isPreventConvert;
            if (convertClazz.equals(PreventConvert.class)){
                throw new BusinessException(BusinessCode.EXCEPTION,"无效的转换");
            }else {
                //如果是A.isAssignableFrom(B) 确定一个类(B)是不是继承来自于另一个父类(A),一个接口(A)是不是实现了另外一个接口(B),或者两个类相同。
                isPreventConvert = PreventConvert.class.isAssignableFrom(convertClazz);
            }
            if (isPreventConvert){
                PreventConvert convert = convertClazz.newInstance();
                try {
                    sb.append(convert.convert(args));
                }catch (Throwable t){
                    log.error("[PreventAop]some errors happens in PreventAop's convert",t);
                }
            }
    
            //自定义策略
            Class strategy = preventAnnotation.strategy();
            boolean isPreventHandler;
            if (strategy.equals(PreventHandler.class)){
                throw new BusinessException(BusinessCode.EXCEPTION,"无效的处理策略");
            }else {
                isPreventHandler = PreventHandler.class.isAssignableFrom(strategy);
            }
            if (isPreventHandler){
                PreventHandler handler = strategy.newInstance();
                try {
                    handler.handle(sb.toString(),preventAnnotation,methodFullName);
                }catch (BusinessException be){
                    throw be;
                }catch (Throwable t){
                    log.error("[PreventAop]some errors happens in PreventAop's strategy",t);
                }
            }
            return;
        }
    }
    
    
    • 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

    3.自定义转换器

    这一步也是必须的,通过实现 PreventConvert 来自定义 redis的key值。

    3.1 PreventConvert 转换器基类

    package com.terrytian.springboottq.convert;
    
    /**
     * 自定义参数转换器
     * @author tianqing
     */
    public interface PreventConvert {
    
        String convert(Object[] args);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3.2 自定义转换器deno

    package com.terrytian.springboottq.convert;
    
    import com.terrytian.springboottq.modules.dto.TestRequest;
    import org.apache.commons.lang3.StringUtils;
    
    /**
     * @program: DevSpace
     * @ClassName DemoConvert
     * @description: demo自定义转换器
     * @author: tianqing
     * @create: 2022-11-26 19:26
     * @Version 1.0
     **/
    public class DemoConvert implements PreventConvert {
    
        @Override
        public String convert(Object[] args) {
            TestRequest testRequest = (TestRequest) args[0];
            return StringUtils.join(testRequest.getMobile());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    4.自定义处理策略

    4.1 处理策略基类 PreventHandler

    package com.terrytian.springboottq.handler;
    
    import com.terrytian.springboottq.annotation.Prevent;
    
    /**
     * @创建人 tianqing
     * @创建时间 2022/11/26
     * @描述 自定义数据处理器
     */
    public interface PreventHandler {
        /**
         *
         * @param partKeyStr 存入redis的部分key
         * @param prevent @PPrevent
         * @param methodFullName 方法全名
         * @throws Exception
         */
        void handle(String partKeyStr, Prevent prevent, String methodFullName) throws Exception;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    4.2 处理策略demo

    package com.terrytian.springboottq.handler;
    
    import com.terrytian.springboottq.annotation.Prevent;
    import com.terrytian.springboottq.common.BusinessCode;
    import com.terrytian.springboottq.common.BusinessException;
    import com.terrytian.springboottq.util.CommonUtils;
    import com.terrytian.springboottq.util.RedisUtil;
    import com.terrytian.springboottq.util.SpringUtil;
    import org.apache.commons.lang3.StringUtils;
    
    /**
     * @program: DevSpace
     * @ClassName DemoHandler
     * @description:
     * @author: tianqing
     * @create: 2022-11-26 19:33
     * @Version 1.0
     **/
    public class DemoHandler implements PreventHandler {
        /**
         *
         * @param partKeyStr 存入redis的部分key
         * @param prevent @PPrevent
         * @param methodFullName 方法全名
         * @throws Exception
         */
        @Override
        public void handle(String partKeyStr, Prevent prevent, String methodFullName) throws Exception {
            String base64Str = CommonUtils.toBase64String(partKeyStr);
    
            long expire = Long.parseLong(prevent.time());
    
            //手动获取redis工具类
            RedisUtil redisUtil = (RedisUtil) SpringUtil.getBean("redisUtil");
    
            String resp = (String) redisUtil.get(methodFullName + base64Str);
            if (StringUtils.isEmpty(resp)) {
                redisUtil.set(methodFullName + base64Str, partKeyStr, expire);
            } else {
                String message = !StringUtils.isEmpty(prevent.message()) ? prevent.message() :
                        expire + "秒内不允许重复请求!";
                throw new BusinessException(BusinessCode.EXCEPTION, message);
            }
    
        }
    }
    
    • 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

    5.使用

    package com.terrytian.springboottq.modules.controller;
    
    import com.terrytian.springboottq.annotation.Prevent;
    import com.terrytian.springboottq.common.Response;
    import com.terrytian.springboottq.convert.DemoConvert;
    import com.terrytian.springboottq.handler.DemoHandler;
    import com.terrytian.springboottq.modules.dto.TestRequest;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 切面实现入参校验
     */
    @RestController
    public class MyController {
    
        /**
         * 测试防刷
         *
         * @param request
         * @return
         */
        @ResponseBody
        @GetMapping(value = "/testPrevent")
        @Prevent
        public Response testPrevent(TestRequest request) {
            return Response.success("调用成功");
        }
    
    
        /**
         * 测试防刷
         *
         * @param request
         * @return
         */
        @ResponseBody
        @GetMapping(value = "/testPreventIncludeMessage")
        @Prevent(convert = DemoConvert.class,message = "10秒内不允许重复调多次", time = "10",strategy = DemoHandler.class)
        public Response testPreventIncludeMessage(TestRequest request) {
            return Response.success("调用成功");
        }
    }
    
    
    • 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

    其它

    1. 以上内容仅展示了主要的代码,详细代码可以参照码云详细代码
    2. 参考文章:https://mp.weixin.qq.com/s?__biz=MzI4OTA3NDQ0Nw==&mid=2455552542&idx=1&sn=cfd65bc5610d8f4506cfc4e25ce5a4ba&chksm=fb9cde7ecceb57687de69a6414ae45ffa3006c146c44bdaed1075ff790e3fd1f6b3da540d77f&scene=126&sessionid=1669361726&subscene=236&key=eb6afd4c0788b4ae1bec69889c3675a9c909d791dd91c7aea1bca5db905d14b005873f62adc602dca3d1f42fc9a9ce2fd6cbae15c84ddba06e26b9fb257bd9e1d287f5e7f8432149b11835d103f7655fe17d9d7d7a22ac00dc288dad12cc3e4473159542db81de4805fc192624720de0f0296198357b5b523b97cec7eaf0fe0e&ascene=7&uin=MTkwMjM4NTUyMQ%3D%3D&devicetype=Windows+11+x64&version=63080029&lang=zh_CN&exportkey=n_ChQIAhIQse2DFnVt2WFETfbeQPG%2BGhLfAQIE97dBBAEAAAAAAMBOEaFVQsIAAAAOpnltbLcz9gKNyK89dVj0aZo1Z%2FBN2Nc3264NaztR3BzDphn0is1jmGojNoZAC6E5b9CiRHW%2BYYjsR%2F4CLEUivXzwpStR5MPEmjqrBjvbfnWZxvtiPXXgLICj0nyR0yEwMZAXBXfnYwM9zW4ujIUtvw%2F54o3WMc04xUkLf6cNjN9zY6xU7g7PmP%2BRS3%2FLpLFKfiz3fkitwJouky0uxKpK431HRzh%2BDoXAbuCjiKaBwLVugXYOvnAVbbsExGsxCjcWSmrSJrlSDB0%3D&acctmode=0&pass_ticket=eEPrLIvI8n6%2FpN7Iz%2BPntiRof9Kl0NjiRwpdaQxLOJ2NPVI51S5YmSeJwtHSbwwN&wx_header=1&fontgear=2
  • 相关阅读:
    数据中台的五个关键要素
    sudo: docker-compose command not found
    接口测试 —— Jmeter 之测试片段的应用
    java计算机毕业设计ssm+vue 大好前途高校毕业生求职招聘网站
    博客网页制作基础大二dw作业 web课程设计网页制作 个人网页设计与实现 我的个人博客网页开发
    js堆栈函数及断点调试(简单使用,仅供自己参考)
    博客园美化显示效果
    单调递增的数字【贪心算法】
    SEGGER调试利器RTT,替代串口,高速数据上传
    【LeetCode】【剑指offer】【扑克牌中的顺子】
  • 原文地址:https://blog.csdn.net/u013255737/article/details/128058665