|
|
@@ -4,8 +4,9 @@ import cn.hutool.core.convert.Convert;
|
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
import com.zsElectric.boot.common.constant.RedisConstants;
|
|
|
import com.zsElectric.boot.common.constant.SystemConstants;
|
|
|
-import com.zsElectric.boot.core.web.ResultCode;
|
|
|
import com.zsElectric.boot.common.util.IPUtils;
|
|
|
+import com.zsElectric.boot.config.property.SecurityProperties;
|
|
|
+import com.zsElectric.boot.core.web.ResultCode;
|
|
|
import com.zsElectric.boot.core.web.WebResponseHelper;
|
|
|
import com.zsElectric.boot.system.service.ConfigService;
|
|
|
import jakarta.servlet.FilterChain;
|
|
|
@@ -15,60 +16,101 @@ import jakarta.servlet.http.HttpServletResponse;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.jetbrains.annotations.NotNull;
|
|
|
import org.springframework.data.redis.core.RedisTemplate;
|
|
|
+import org.springframework.data.redis.core.script.DefaultRedisScript;
|
|
|
+import org.springframework.data.redis.core.script.RedisScript;
|
|
|
+import org.springframework.http.HttpStatus;
|
|
|
import org.springframework.web.filter.OncePerRequestFilter;
|
|
|
|
|
|
import java.io.IOException;
|
|
|
-import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.Collections;
|
|
|
|
|
|
/**
|
|
|
* IP 限流过滤器
|
|
|
*
|
|
|
+ * <p>基于 Redis 记录单个 IP 每秒请求次数。Redis 异常时按配置降级处理,避免限流依赖故障导致登录和接口整体 500。</p>
|
|
|
+ *
|
|
|
* @author Theo
|
|
|
* @since 2024/08/10 14:38
|
|
|
*/
|
|
|
@Slf4j
|
|
|
public class RateLimiterFilter extends OncePerRequestFilter {
|
|
|
|
|
|
+ /**
|
|
|
+ * 使用 Lua 保证 INCR 与首次 EXPIRE 在 Redis 侧原子执行,避免计数键漏设过期时间。
|
|
|
+ */
|
|
|
+ private static final String RATE_LIMIT_SCRIPT_TEXT = """
|
|
|
+ local current = redis.call('INCR', KEYS[1])
|
|
|
+ if current == 1 then
|
|
|
+ redis.call('EXPIRE', KEYS[1], ARGV[1])
|
|
|
+ end
|
|
|
+ return current
|
|
|
+ """;
|
|
|
+ private static final RedisScript<Long> RATE_LIMIT_SCRIPT = new DefaultRedisScript<>(RATE_LIMIT_SCRIPT_TEXT, Long.class);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * IP 限流时间窗口,单位:秒
|
|
|
+ */
|
|
|
+ private static final long RATE_LIMIT_WINDOW_SECONDS = 1L;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 默认 IP 限流阈值
|
|
|
+ */
|
|
|
+ private static final long DEFAULT_IP_LIMIT = 1000L;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 限流服务不可用时返回给客户端的明确提示
|
|
|
+ */
|
|
|
+ private static final String RATE_LIMIT_SERVICE_UNAVAILABLE_MESSAGE = "限流服务异常,请稍后再试";
|
|
|
+
|
|
|
private final RedisTemplate<String, Object> redisTemplate;
|
|
|
private final ConfigService configService;
|
|
|
+ private final SecurityProperties.RateLimiterConfig rateLimiterConfig;
|
|
|
|
|
|
- private static final long DEFAULT_IP_LIMIT = 1000L; // 默认 IP 限流阈值
|
|
|
-
|
|
|
- public RateLimiterFilter(RedisTemplate<String, Object> redisTemplate, ConfigService configService) {
|
|
|
+ public RateLimiterFilter(RedisTemplate<String, Object> redisTemplate,
|
|
|
+ ConfigService configService,
|
|
|
+ SecurityProperties.RateLimiterConfig rateLimiterConfig) {
|
|
|
this.redisTemplate = redisTemplate;
|
|
|
this.configService = configService;
|
|
|
+ this.rateLimiterConfig = rateLimiterConfig == null ? new SecurityProperties.RateLimiterConfig() : rateLimiterConfig;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 判断 IP 是否触发限流
|
|
|
- * 默认限制同一 IP 每秒最多请求 10 次,可通过系统配置调整。
|
|
|
+ * 判断 IP 限流结果
|
|
|
+ * 默认限制同一 IP 每秒最多请求 1000 次,可通过系统配置调整。
|
|
|
* 如果系统未配置限流阈值,默认跳过限流。
|
|
|
*
|
|
|
* @param ip IP 地址
|
|
|
- * @return 是否限流:true 表示限流;false 表示未限流
|
|
|
+ * @return 限流判定结果
|
|
|
*/
|
|
|
- public boolean rateLimit(String ip) {
|
|
|
- // 限流 Redis 键
|
|
|
- String key = StrUtil.format(RedisConstants.RateLimiter.IP, ip);
|
|
|
-
|
|
|
- // 自增请求计数
|
|
|
- Long count = redisTemplate.opsForValue().increment(key);
|
|
|
- if (count == null || count == 1) {
|
|
|
- // 第一次访问时设置过期时间为 1 秒
|
|
|
- redisTemplate.expire(key, 1, TimeUnit.SECONDS);
|
|
|
- }
|
|
|
+ RateLimitDecision rateLimit(String ip) {
|
|
|
+ try {
|
|
|
+ // 获取系统配置的限流阈值
|
|
|
+ Object systemConfig = configService.getSystemConfig(SystemConstants.SYSTEM_CONFIG_IP_QPS_LIMIT_KEY);
|
|
|
+ if (systemConfig == null) {
|
|
|
+ log.warn("系统未配置限流阈值,跳过限流");
|
|
|
+ return RateLimitDecision.ALLOW;
|
|
|
+ }
|
|
|
|
|
|
- // 获取系统配置的限流阈值
|
|
|
- Object systemConfig = configService.getSystemConfig(SystemConstants.SYSTEM_CONFIG_IP_QPS_LIMIT_KEY);
|
|
|
- if (systemConfig == null) {
|
|
|
- // 系统未配置限流,跳过限流逻辑
|
|
|
- log.warn("系统未配置限流阈值,跳过限流");
|
|
|
- return false;
|
|
|
- }
|
|
|
+ // 执行 Redis 计数脚本,返回当前窗口内该 IP 的请求次数
|
|
|
+ long limit = Convert.toLong(systemConfig, DEFAULT_IP_LIMIT);
|
|
|
+ String key = StrUtil.format(RedisConstants.RateLimiter.IP, ip);
|
|
|
+ Long count = redisTemplate.execute(RATE_LIMIT_SCRIPT, Collections.singletonList(key), RATE_LIMIT_WINDOW_SECONDS);
|
|
|
+ if (count == null) {
|
|
|
+ throw new IllegalStateException("Redis 限流脚本返回空结果");
|
|
|
+ }
|
|
|
|
|
|
- // 转换系统配置为限流值,默认为 10
|
|
|
- long limit = Convert.toLong(systemConfig, DEFAULT_IP_LIMIT);
|
|
|
- return count != null && count > limit;
|
|
|
+ return count > limit ? RateLimitDecision.RATE_LIMITED : RateLimitDecision.ALLOW;
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 限流依赖异常只影响限流能力,不应默认拖垮登录和业务接口。
|
|
|
+ SecurityProperties.RedisFailureStrategy failureStrategy = rateLimiterConfig.getRedisFailureStrategy();
|
|
|
+ if (failureStrategy == SecurityProperties.RedisFailureStrategy.REJECT) {
|
|
|
+ log.error("IP限流依赖异常,按配置拒绝请求,ip={}", ip, e);
|
|
|
+ return RateLimitDecision.SERVICE_UNAVAILABLE;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.error("IP限流依赖异常,降级放行,ip={}", ip, e);
|
|
|
+ return RateLimitDecision.ALLOW;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -80,19 +122,50 @@ public class RateLimiterFilter extends OncePerRequestFilter {
|
|
|
* @param filterChain 过滤器链
|
|
|
*/
|
|
|
@Override
|
|
|
- protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
|
|
|
+ protected void doFilterInternal(@NotNull HttpServletRequest request,
|
|
|
+ @NotNull HttpServletResponse response,
|
|
|
@NotNull FilterChain filterChain) throws ServletException, IOException {
|
|
|
// 获取请求的 IP 地址
|
|
|
String ip = IPUtils.getIpAddr(request);
|
|
|
+ RateLimitDecision decision = rateLimit(ip);
|
|
|
|
|
|
- // 判断是否限流
|
|
|
- if (rateLimit(ip)) {
|
|
|
- // 返回限流错误信息
|
|
|
+ if (decision == RateLimitDecision.RATE_LIMITED) {
|
|
|
+ // 返回正常限流错误信息
|
|
|
WebResponseHelper.writeError(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED);
|
|
|
return;
|
|
|
}
|
|
|
+ if (decision == RateLimitDecision.SERVICE_UNAVAILABLE) {
|
|
|
+ // Redis 异常且配置为拒绝时,返回明确的限流服务异常,避免继续冒泡成 500。
|
|
|
+ WebResponseHelper.writeError(
|
|
|
+ response,
|
|
|
+ HttpStatus.SERVICE_UNAVAILABLE.value(),
|
|
|
+ ResultCode.SYSTEM_FUNCTION_DEGRADATION,
|
|
|
+ RATE_LIMIT_SERVICE_UNAVAILABLE_MESSAGE
|
|
|
+ );
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
// 未触发限流,继续执行过滤器链
|
|
|
filterChain.doFilter(request, response);
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * IP 限流判定结果
|
|
|
+ */
|
|
|
+ enum RateLimitDecision {
|
|
|
+ /**
|
|
|
+ * 允许请求继续执行
|
|
|
+ */
|
|
|
+ ALLOW,
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 请求超过配置阈值
|
|
|
+ */
|
|
|
+ RATE_LIMITED,
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 限流服务不可用,且配置要求拒绝请求
|
|
|
+ */
|
|
|
+ SERVICE_UNAVAILABLE
|
|
|
+ }
|
|
|
}
|