本文主要介绍一种通过实现自定义注解,实现一种比较通用的接口防刷方式
前言
1.基本准备
- jdk 8
- redis
- 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("调用成功");
}
}
其它
- 以上内容仅展示了主要的代码,详细代码可以参照码云详细代码
- 参考文章: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