接口限制是为了防止用户恶意请求或者接口被攻击的一种防御手段。当然有很多维度的防御手段。
本文采用Springboot框架中的AOP实现自定义注解对接口的请求频率进行限制。
注解基础可以自行百度或者查看该文章,内有较为详细的介绍
1.创建一个注解类
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author hxy
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface VisitLimit {
/**
* 请求方法
*/
String value();
/**
*设置过期时间
*/
long timeOut() default 20;
/**
* 过期时间内的访问次数
*/
int number() default 10;
/**
* 访问者IP
*/
String ip() default "";
}
2.创建一个AOP类
import com.tdkj.DataApi.Response.Response;
import com.tdkj.DataApi.annotation.VisitLimit;
import com.tdkj.DataApi.service.RedisService;
import com.tdkj.DataApi.utils.IpUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
/**
* @author Administrator
* @Aspect 声明是个切面的类(必要)
* @Component 将该类交给spring容器管理(必要)
* @Slf4j 日志输出
*/
@Aspect
@Component
@Slf4j
public class VisitLimitAspect {
@Resource
private RedisService redisService;
/**
* @Pointct 表示该方法可用于某个 这个注解
* 还可以使用*号表示通配符
*/
@Pointcut("@annotation(com.tdkj.DataApi.annotation.VisitLimit)")
public void pointcut() {
}
/**
* @return
* @Around 环绕通知
* @Around("pointcut()") 可以理解为对这个方法进行环绕通知
* ProceedingJoinPoint 参数 用于环绕通知,
* 使用proceed()方法来执行目标方法,可以理解为 前置通知结束 开始执行使用该注解的方法。
*/
@Around("pointcut()")
public Object checkTokenAroundAspect(ProceedingJoinPoint joinPoint) throws Throwable {
//获取请求头
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//调用方法获取请求地址
String ip = IpUtil.getIpAddr(request);
//获取调用的方法
Method method = this.getMethod(joinPoint);
//通过method对象获取到使用该注解的方法。就可以获取到通过注解传过来的参数。
VisitLimit visitLimit = method.getAnnotation(VisitLimit.class);
/**
* 所需参数
*/
//获取注解上的参数
String value = visitLimit.value();
//过期时间
long timeOut = visitLimit.timeOut();
//最大次数(和过期时间相同使用)
int number = visitLimit.number();
//ip+value 可以保证用户在同一时间内访问其他方法时
//不会被继续限制,而会重新计数
String key = ip + "-" + value;
/**
* 判断逻辑
*/
//1.判断当前key是否存在。如果存在则证明20秒内请求过至少一次
if (redisService.hasKey(key)) {
//根据key获取value值
Integer i = (Integer) redisService.get(key);
//根据key获取过期时间(还剩多久过期)
Long time = redisService.getExpire(key);
//如果过期时间小于2秒,请求次数等于10次 就return
if (time < timeOut && i == number) {
return new Response().error().message("请求频繁");
}
//修改值而不刷新缓存时间
//可看文章最后
redisService.setRange(key, ++i, 0);
System.out.println(key + "," + i);
} else {
//如果没有请求过则新增缓存,设置第一次请求,并设置过期时间
redisService.set(key, 1, timeOut);
System.out.println(key + "," + 1);
}
//继续执行方法,并获取方法返回值
Object result = joinPoint.proceed();
return result;
}
/**
* 获取调用的方法
*/
private Method getMethod(ProceedingJoinPoint jp) throws Exception {
MethodSignature methodSignature = (MethodSignature) jp.getSignature();
Method method = methodSignature.getMethod();
return jp.getTarget().getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
}
}
3.测试
@VisitLimit(value = "test")
@PostMapping(value = "test")
@ResponseBody
public Response test() {
System.out.println("我是方法内输出");
return new Response().success().message("成功");
}
通过postMan进行循环调用测试
测试结果
redis结果
完成了。
/**
*
* @author Hxy
* @Description 设置偏移量为0时,即可只更新value而不刷新缓存时间(还不清楚为啥,但是能用)
* 有缺陷就是。修改后的value长度不可低于修改前的value长度
* @date 2021/7/15 8:45
* @params [key, value, offset]
* @return java.lang.Boolean
* @version 1.0.0
*/
public Boolean setRange(String key, Object value, long offset) {
try {
redisTemplate.opsForValue().set(key, value, offset);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}