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

前言

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.基本准备

  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

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

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
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 转换器基类

1
2
3
4
5
6
7
8
9
10
11
package com.terrytian.springboottq.convert;

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

String convert(Object[] args);
}

3.2 自定义转换器deno

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

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
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.使用

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