跳到主要内容

一个注解实现接口防刷

我想要实现的功能是一个接口,在x秒内不能被多次调用,也就是只能调用一次,为了防止别人刷接口,下面使用注解来实现这个功能

我们定义一个注解Prevent

@Target({ElementType.METHOD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Prevent {

/**
* 限制的时间值(秒)
*/
String value() default "0";

/**
* 提示
*/
String message() default "操作过于频繁,请稍后再试";

/**
* 策略
*/
PreventStrategy strategy() default PreventStrategy.DEFAULT;
}

@Target({ElementType.METHOD}):指定了该注解可以应用在方法上,表示该注解用于限制某些方法的执行。

@Documented:表示该注解应该被包含在 Java 文档中。

@Retention(RetentionPolicy.RUNTIME):指定了注解的保留策略是运行时,这意味着注解可以在运行时通过反射获取到。

具体的策略类是一个枚举:

public enum PreventStrategy {
/**
* 默认策略
*/
DEFAULT,
/**
* 限制时间内
*/
LIMIT_TIME,
/**
* 限制次数
*/
LIMIT_COUNT
}

接着写一个AOP切面

/**
* @author houyunfei
*/
@Aspect
@Component
public class PreventAop {
@Autowired
private StringRedisTemplate stringRedisTemplate;

/**
* 切入点
*/
@Pointcut("@annotation(com.totoro.web.controller.Prevent)")
public void prevent() {
}

/**
* 前置通知
*/
@Before("prevent()")
public void jointPoint(JoinPoint joinPoint) throws Exception {
String requestStr = JsonUtils.toJson(joinPoint.getArgs()[0]);
if (requestStr.isEmpty() || "{}".equalsIgnoreCase(requestStr)) {
throw new Exception("【防刷】入参不允许为空");
}
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = joinPoint.getTarget().getClass().getMethod(methodSignature.getName()
, methodSignature.getParameterTypes());
Prevent preventAnnotation = method.getAnnotation(Prevent.class);
String methodFullName = method.getDeclaringClass().getName() + method.getName();
entrance(preventAnnotation, requestStr, methodFullName);
}

private void entrance(Prevent preventAnnotation, String requestStr, String methodFullName) throws Exception {
PreventStrategy strategy = preventAnnotation.strategy();
switch (strategy) {
case DEFAULT:
defaultHandle(requestStr, preventAnnotation, methodFullName);
break;
default:
throw new BaseException("【防刷】不支持的策略");
}

}

/**
* 默认处理方式
*
* @param requestStr
* @param prevent
*/
private void defaultHandle(String requestStr, Prevent prevent, String methodFullName) throws Exception {
String base64Str = toBase64String(requestStr);
long expire = Long.parseLong(prevent.value());
String resp = stringRedisTemplate.opsForValue().get(methodFullName + base64Str);
if (StringUtils.isEmpty(resp)) {
stringRedisTemplate.opsForValue().set(methodFullName + base64Str, requestStr, expire, TimeUnit.SECONDS);
} else {
String message = !StringUtils.isEmpty(prevent.message()) ? prevent.message() :
expire + "秒内不允许重复请求";
throw new BaseException("【防刷】" + message);
}
}


/**
* 对象转换为base64字符串
*
* @param obj 对象值
* @return base64字符串
*/
private String toBase64String(String obj) throws Exception {
if (StringUtils.isEmpty(obj)) {
return null;
}
Base64.Encoder encoder = Base64.getEncoder();
byte[] bytes = obj.getBytes("UTF-8");
return encoder.encodeToString(bytes);
}
}

@Pointcut:定义了一个切入点,@annotation(com.totoro.web.controller.Prevent) 表示带有 @Prevent 注解的方法会触发该切面。

@Before:定义了一个前置通知,在带有 @Prevent 注解的方法执行之前执行。

最后就可以在方法上使用了:

@GetMapping("/test/{id}")
@Prevent(value = "10", message = "【防刷】你这种小人,请勿频繁操作")
public BaseRespDTO test(@PathVariable String id) {
LocalDateTime now = LocalDateTime.now();
System.out.println("id: " + id + ", now: " + now);
return BaseRespDTO.success(now);
}

连续点击两次效果:

image-20240728142412035