1. 介绍
参考b站up主极海,实现一个简单的接口限流方案,基于IP+方法(粒度)实现,可以简单防止接口攻击,限制某个IP频繁访问接口某个方法等,避免恶意刷接口造成的服务器故障。
2. 实现方案
- 采用aop思想实现,对全部controller层方法进行限制。
- 限流方式:RateLimiter + Cache(Guava),采用Cache存储某个”ip+方法“对应的RateLimiterr,并设置过期策略。
2.1 导入jar包
<!--Guava-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.1-jre</version>
</dependency>
<!--aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 测试接口
package com.example.webtest.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Janson
* @Description 测试接口
* @Date 2023/7/10
*/
@RestController
@RequestMapping("")
public class RatelimterTest {
@GetMapping("testGet")
public String testGet(){
return "testGet";
}
}
2.3 限流接口
package com.example.webtest.around;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.util.concurrent.RateLimiter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
/**
* @author Janson
* @Description 简单接口限流方案
* @Date 2023/7/10
*/
@Component
@Aspect
public class IpLimiterAspect {
// 每秒创建一个令牌,令牌越多,每秒支持的请求次数越多。
private int DEFAULT_LIMITER_COUNT_PER_SECOND = 1;
// 创建guava缓存,给的过期时间,防止缓存数据量过大
Cache<String, RateLimiter> limiterCache = CacheBuilder
.newBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES)
.build();
@Autowired
private HttpServletRequest httpServletRequest;
/*
* 匹配路径:* com.example.webtest.web..*.*(..)
* 1. 第一个 * : 支持任意返回结果;
* 2. web后面 .. , 表示覆盖web包及其子包,如果是 . ,则中覆盖web包下的接口,不包含web子包接口;
* 3. 第二个 * :web包及其子包下所以类;
* 4. 第三个 * : 所有方法;
* 5. (..) : 表示方法可以支持任意参数。
* */
@Around("execution(* com.example.webtest.web..*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 如果采用了nginx代理,前端在请求时需要在header中写入真实ip: X-Real-IP = ip
String ip = httpServletRequest.getHeader("X-Real-IP");
// 没有采用nginx,则直接获取远程主机ip
String remoteHost = httpServletRequest.getRemoteHost();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
// 获取类名称
String name = joinPoint.getTarget().getClass().getName();
// 获取方法名称
String name2 = methodSignature.getName();
String methodName = name + "." + name2;
// 拼接缓存key
String recodeKey = ip + "->" + methodName;
// 尝试从缓存中获取key值为recodeKey 的缓存rateLimiter,如果获取不到,则创建一个rateLimiter
RateLimiter rateLimiter = limiterCache.get(recodeKey,
() -> RateLimiter.create(DEFAULT_LIMITER_COUNT_PER_SECOND));
// 尝试获取令牌,拿不到令牌则禁止访问。
if (!rateLimiter.tryAcquire()){
System.out.println("操作太频繁,限流了");
return "操作失败";
}
return joinPoint.proceed();
}
}
以上实现了一个简单的接口限流(防刷)方案,一般对于轻量级项目,可以解决接口防攻击问题。