Springboot基於Guava+自定義注解實現IP或自定義key限流 升級版
2020年5月17日 凌晨 有人惡意刷接口,剛喝完酒回來 大晚上的給我搞事情。。。。
之前版本Springboot基於Guava+自定義注解實現限流功能是對訪問這個接口所有人總的QPS限制,如果我們想對某一個用戶或Ip地址訪問接口的QPS限制,限制惡意請求接口的人而不影響正常的用戶請求訪問。
實現步驟
1、添加POM依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--AOP相關-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
2、定義注解
package com.example.guavalimit.limit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Description 自定義限流注解
* @Author jie.zhao
* @Date 2020/5/17 11:49
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LxRateLimit {
//資源名稱
String name() default "默認資源";
//限制每秒訪問次數,默認為3次
double perSecond() default 3;
/**
* 限流Key類型
* 自定義根據業務唯一碼來限制需要在請求參數中添加 String limitKeyValue
*/
LimitKeyTypeEnum limitKeyType() default LimitKeyTypeEnum.IPADDR;
}
3、定義切面
package com.example.guavalimit.limit;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.RateLimiter;
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.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* @Description 基於Guava cache緩存存儲實現限流切面
* @Author jie.zhao
* @Date 2020/5/17 11:51
*/
@Slf4j
@Aspect
@Component
public class LxRateLimitAspect {
/**
* 緩存
* maximumSize 設置緩存個數
* expireAfterWrite 寫入后過期時間
*/
private static LoadingCache<String, RateLimiter> limitCaches = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.DAYS)
.build(new CacheLoader<String, RateLimiter>() {
@Override
public RateLimiter load(String key) throws Exception {
double perSecond = LxRateLimitUtil.getCacheKeyPerSecond(key);
return RateLimiter.create(perSecond);
}
});
/**
* 切點
* 通過掃包切入 @Pointcut("execution(public * com.ycn.springcloud.*.*(..))")
* 帶有指定注解切入 @Pointcut("@annotation(com.ycn.springcloud.annotation.LxRateLimit)")
*/
@Pointcut("@annotation(com.example.guavalimit.limit.LxRateLimit)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
log.info("限流攔截到了{}方法...", point.getSignature().getName());
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
if (method.isAnnotationPresent(LxRateLimit.class)) {
String cacheKey = LxRateLimitUtil.generateCacheKey(method, request);
RateLimiter limiter = limitCaches.get(cacheKey);
if (!limiter.tryAcquire()) {
throw new LimitAccessException("【限流】這位小同志的手速太快了");
}
}
return point.proceed();
}
}
4、枚舉
package com.example.guavalimit.limit;
/**
* @Description 限流key類型枚舉
* @Author jie.zhao
* @Date 2020/5/17 14:28
*/
public enum LimitKeyTypeEnum {
IPADDR("IPADDR", "根據Ip地址來限制"),
CUSTOM("CUSTOM", "自定義根據業務唯一碼來限制,需要在請求參數中添加 String limitKeyValue");
private String keyType;
private String desc;
LimitKeyTypeEnum(String keyType, String desc) {
this.keyType = keyType;
this.desc = desc;
}
public String getKeyType() {
return keyType;
}
public String getDesc() {
return desc;
}
}
5、工具類
package com.example.guavalimit.limit;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @Description 限流工具類
* @Author jie.zhao
* @Date 2020/5/17 15:37
*/
public class LxRateLimitUtil {
/**
* 獲取唯一key根據注解類型
* <p>
* 規則 資源名:業務key:perSecond
*
* @param method
* @param request
* @return
*/
public static String generateCacheKey(Method method, HttpServletRequest request) {
//獲取方法上的注解
LxRateLimit lxRateLimit = method.getAnnotation(LxRateLimit.class);
StringBuffer cacheKey = new StringBuffer(lxRateLimit.name() + ":");
switch (lxRateLimit.limitKeyType()) {
case IPADDR:
cacheKey.append(getIpAddr(request) + ":");
break;
case CUSTOM:
String limitKeyValue = request.getParameter("limitKeyValue");
if (StringUtils.isEmpty(limitKeyValue)) {
throw new LimitAccessException("【" + method.getName() + "】自定義業務Key缺少參數String limitKeyValue,或者參數為空");
}
cacheKey.append(limitKeyValue + ":");
break;
}
cacheKey.append(lxRateLimit.perSecond());
return cacheKey.toString();
}
/**
* 獲取緩存key的限制每秒訪問次數
* <p>
* 規則 資源名:業務key:perSecond
*
* @param cacheKey
* @return
*/
public static double getCacheKeyPerSecond(String cacheKey) {
String perSecond = cacheKey.split(":")[2];
return Double.parseDouble(perSecond);
}
/**
* 獲取客戶端IP地址
*
* @param request 請求
* @return
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if ("127.0.0.1".equals(ip)) {
//根據網卡取本機配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ip = inet.getHostAddress();
}
}
// 對於通過多個代理的情況,第一個IP為客戶端真實IP,多個IP按照','分割
if (ip != null && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
if ("0:0:0:0:0:0:0:1".equals(ip)) {
ip = "127.0.0.1";
}
return ip;
}
}
6、自定義異常
package com.example.guavalimit.limit;
/**
* @Description 限流自定義異常
* @Author jie.zhao
* @Date 2019/8/7 16:01
*/
public class LimitAccessException extends RuntimeException {
private static final long serialVersionUID = -3608667856397125671L;
public LimitAccessException(String message) {
super(message);
}
}
7、測試controller
package com.example.guavalimit.controller;
import com.example.guavalimit.limit.LimitKeyTypeEnum;
import com.example.guavalimit.limit.LxRateLimit;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/test1")
@LxRateLimit(perSecond = 1, limitKeyType = LimitKeyTypeEnum.IPADDR)
public String test1() {
return "SUCCESS";
}
@GetMapping("/test2")
@LxRateLimit(perSecond = 1, limitKeyType = LimitKeyTypeEnum.CUSTOM)
public String test2(String limitKeyValue) {
return "SUCCESS";
}
}