本文主要介绍一种通过实现自定义注解,实现一种比较通用的接口防刷方式
主要就是借助 redis 来实现接口的防刷。
基本逻辑:定义一个切面,通过@Prevent注解作为切入点、在该切面的前置通知获取该方法的所有入参;
同时,通过@Prevent注解的convert属性,自定义redis的部分key值,并将其Base64编码+完整方法名作为redis的key,
自定义redis的部分key值作为reids的value,@Prevent的time作为redis的expire,存入redis;
每次进来这个切面根据自定义入参Base64编码+完整方法名判断redis值是否存在,存在则拦截防刷,不存在则允许调用;
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 extends PreventConvert> nullAble() default PreventConvert.class;
/**
* 转换器:用于定义redis的key
* @return
*/
Class extends PreventConvert> convert() default PreventConvert.class;
/**
* 处理策略
* @return
*/
Class extends PreventHandler> strategy() default PreventHandler.class;
}
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;
}
}
这一步也是必须的,通过实现 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.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);
}
}
}
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("调用成功");
}
}