|
|
@@ -4,6 +4,8 @@ import com.google.common.base.Strings;
|
|
|
import com.tzld.piaoquan.api.annotation.RateLimit;
|
|
|
import com.tzld.piaoquan.api.common.enums.ExceptionEnum;
|
|
|
import com.tzld.piaoquan.api.common.exception.CommonException;
|
|
|
+import com.tzld.piaoquan.api.model.config.LoginUserContext;
|
|
|
+import com.tzld.piaoquan.api.model.po.contentplatform.ContentPlatformAccount;
|
|
|
import com.tzld.piaoquan.growth.common.utils.IpUtil;
|
|
|
import com.tzld.piaoquan.growth.common.utils.RedisUtils;
|
|
|
import org.aspectj.lang.ProceedingJoinPoint;
|
|
|
@@ -23,66 +25,77 @@ import java.lang.reflect.Method;
|
|
|
import java.util.Objects;
|
|
|
|
|
|
/**
|
|
|
- * IP 限流 AOP 切面
|
|
|
+ * 限流 AOP 切面,支持按账号或 IP 维度
|
|
|
*/
|
|
|
@Aspect
|
|
|
@Component
|
|
|
public class RateLimitAop {
|
|
|
-
|
|
|
+
|
|
|
private static final Logger log = LoggerFactory.getLogger(RateLimitAop.class);
|
|
|
-
|
|
|
+
|
|
|
@Autowired
|
|
|
private RedisUtils redisUtils;
|
|
|
-
|
|
|
+
|
|
|
@Pointcut("@annotation(com.tzld.piaoquan.api.annotation.RateLimit)")
|
|
|
public void rateLimit() {
|
|
|
}
|
|
|
-
|
|
|
- /**
|
|
|
- * 环绕通知,执行限流逻辑
|
|
|
- */
|
|
|
+
|
|
|
@Around("rateLimit()")
|
|
|
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
|
|
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
|
|
Method method = signature.getMethod();
|
|
|
-
|
|
|
- // 获取注解信息
|
|
|
+
|
|
|
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
|
|
|
long timeWindow = rateLimit.timeWindow();
|
|
|
long maxRequests = rateLimit.maxRequests();
|
|
|
String message = rateLimit.message();
|
|
|
-
|
|
|
- // 获取请求对象
|
|
|
- ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
|
|
- HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
|
|
|
-
|
|
|
- // 获取客户端 IP
|
|
|
- String clientIp = IpUtil.getIpAddr(request);
|
|
|
-
|
|
|
- if (Strings.isNullOrEmpty(clientIp)) {
|
|
|
- log.warn("Rate limit - client IP is empty");
|
|
|
+ boolean limitByAccount = rateLimit.limitByAccount();
|
|
|
+
|
|
|
+ String methodName = method.getName();
|
|
|
+ String limitKey;
|
|
|
+
|
|
|
+ if (limitByAccount) {
|
|
|
+ ContentPlatformAccount user = LoginUserContext.getUser();
|
|
|
+ if (user == null || user.getId() == null) {
|
|
|
+ log.warn("Rate limit - limitByAccount=true but no login user, fallback to IP");
|
|
|
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
|
|
+ HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
|
|
|
+ limitKey = IpUtil.getIpAddr(request);
|
|
|
+ } else {
|
|
|
+ limitKey = String.valueOf(user.getId());
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
|
|
+ HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
|
|
|
+ limitKey = IpUtil.getIpAddr(request);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Strings.isNullOrEmpty(limitKey)) {
|
|
|
+ log.warn("Rate limit - limit key is empty");
|
|
|
return joinPoint.proceed();
|
|
|
}
|
|
|
-
|
|
|
- // 构建 Redis key: rate_limit:{method_name}:{ip}
|
|
|
- String methodName = method.getName();
|
|
|
- String redisKey = String.format("rate_limit:%s:%s", methodName, clientIp);
|
|
|
-
|
|
|
- // 获取当前请求次数
|
|
|
- Long currentRequests = redisUtils.getLong(redisKey);
|
|
|
-
|
|
|
- if (currentRequests >= maxRequests) {
|
|
|
- log.warn("Rate limit exceeded - IP: {}, Method: {}, Requests: {}, Max: {}",
|
|
|
- clientIp, methodName, currentRequests, maxRequests);
|
|
|
+
|
|
|
+ String redisKey = String.format("rate_limit:%s:%s", methodName, limitKey);
|
|
|
+
|
|
|
+ // 原子递增并返回递增后的值,避免 get + set 的竞态问题
|
|
|
+ Long currentRequests = redisUtils.incrWithExpire(redisKey, timeWindow);
|
|
|
+
|
|
|
+ if (currentRequests == null) {
|
|
|
+ log.warn("Rate limit - incrWithExpire returned null");
|
|
|
+ return joinPoint.proceed();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (currentRequests > maxRequests) {
|
|
|
+ // 回滚本次递增
|
|
|
+ redisUtils.setDecrementValue(redisKey, 1);
|
|
|
+ log.warn("Rate limit exceeded - Key: {}, Method: {}, Requests: {}, Max: {}",
|
|
|
+ limitKey, methodName, currentRequests, maxRequests);
|
|
|
throw new CommonException(ExceptionEnum.PARAM_ERROR.getCode(), message);
|
|
|
}
|
|
|
-
|
|
|
- // 增加请求计数
|
|
|
- redisUtils.setIncrementValue(redisKey, 1, timeWindow);
|
|
|
-
|
|
|
- log.debug("Rate limit - IP: {}, Method: {}, Current Requests: {}, Max: {}",
|
|
|
- clientIp, methodName, currentRequests + 1, maxRequests);
|
|
|
-
|
|
|
+
|
|
|
+ log.debug("Rate limit - Key: {}, Method: {}, Current Requests: {}, Max: {}",
|
|
|
+ limitKey, methodName, currentRequests, maxRequests);
|
|
|
+
|
|
|
return joinPoint.proceed();
|
|
|
}
|
|
|
}
|