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;
 * <p>
 *使用:
 * 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<? extends PreventConvert> nullAble() default PreventConvert.class;

    /**
     * 转换器:用于定义redis的key
     * @return
     */
    Class<? extends PreventConvert> convert() default PreventConvert.class;

    /**
     * 处理策略
     * @return
     */
    Class<? extends PreventHandler> strategy() default PreventHandler.class;
}

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<? extends PreventConvert> 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<? extends PreventConvert> 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<? extends PreventHandler> 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;
    }
}

3.自定义转换器

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

3.1 PreventConvert 转换器基类

package com.terrytian.springboottq.convert;

/**
 * 自定义参数转换器
 * @author tianqing
 */
public interface PreventConvert {

    String convert(Object[] args);
}

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());
    }
}

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;
}

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);
        }

    }
}

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. 参考文章: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
  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值