|
|
@@ -0,0 +1,96 @@
|
|
|
+package com.etotem.cfc.config;
|
|
|
+
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.springframework.web.servlet.HandlerInterceptor;
|
|
|
+
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
+import javax.servlet.http.HttpServletResponse;
|
|
|
+import java.util.Deque;
|
|
|
+import java.util.LinkedList;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 滑动窗口限流拦截器(进程内,基于 IP+URI)
|
|
|
+ * 限制:高频接口 10次/秒,发送验证码 1次/60秒
|
|
|
+ * 注意:多实例部署时需替换为 Redis 方案
|
|
|
+ */
|
|
|
+@Component
|
|
|
+public class RateLimitInterceptor implements HandlerInterceptor {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(RateLimitInterceptor.class);
|
|
|
+
|
|
|
+ private final Map<String, Deque<Long>> requestLogs = new ConcurrentHashMap<>();
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+
|
|
|
+ /** 限流配置:key模式 → {窗口毫秒, 允许次数} */
|
|
|
+ private static final Map<String, long[]> RATE_LIMIT_RULES = new ConcurrentHashMap<>();
|
|
|
+
|
|
|
+ static {
|
|
|
+ // 认证接口:每 IP 每秒 10 次
|
|
|
+ RATE_LIMIT_RULES.put("/api/auth/", new long[]{1000L, 10});
|
|
|
+ RATE_LIMIT_RULES.put("/api/admin-auth/", new long[]{1000L, 10});
|
|
|
+ // 验证码发送接口:每 IP 每 60 秒 1 次
|
|
|
+ RATE_LIMIT_RULES.put("/api/auth/send-code", new long[]{60000L, 1});
|
|
|
+ // 默认:每 IP 每秒 50 次
|
|
|
+ RATE_LIMIT_RULES.put("__default__", new long[]{1000L, 50});
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
|
|
+ String path = request.getRequestURI();
|
|
|
+ String clientIp = getClientIp(request);
|
|
|
+ String key = clientIp + ":" + path;
|
|
|
+
|
|
|
+ long[] rule = findRule(path);
|
|
|
+ long windowMs = rule[0];
|
|
|
+ int maxRequests = (int) rule[1];
|
|
|
+
|
|
|
+ long now = System.currentTimeMillis();
|
|
|
+
|
|
|
+ Deque<Long> timestamps = requestLogs.computeIfAbsent(key, k -> new LinkedList<>());
|
|
|
+
|
|
|
+ synchronized (timestamps) {
|
|
|
+ // 清理过期记录
|
|
|
+ while (!timestamps.isEmpty() && timestamps.peekFirst() < now - windowMs) {
|
|
|
+ timestamps.pollFirst();
|
|
|
+ }
|
|
|
+ if (timestamps.size() >= maxRequests) {
|
|
|
+ log.warn("限流触发: key={}, count={}, window={}ms", key, timestamps.size(), windowMs);
|
|
|
+ response.setStatus(429);
|
|
|
+ response.setContentType("application/json;charset=UTF-8");
|
|
|
+ response.getWriter().write("{\"code\":429,\"message\":\"请求过于频繁,请稍后再试\",\"data\":null}");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ timestamps.addLast(now);
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private long[] findRule(String path) {
|
|
|
+ for (Map.Entry<String, long[]> entry : RATE_LIMIT_RULES.entrySet()) {
|
|
|
+ if (path.contains(entry.getKey())) {
|
|
|
+ return entry.getValue();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return RATE_LIMIT_RULES.get("__default__");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getClientIp(HttpServletRequest request) {
|
|
|
+ String ip = request.getHeader("X-Forwarded-For");
|
|
|
+ if (ip == null || ip.isEmpty()) {
|
|
|
+ ip = request.getHeader("X-Real-IP");
|
|
|
+ }
|
|
|
+ if (ip == null || ip.isEmpty()) {
|
|
|
+ ip = request.getRemoteAddr();
|
|
|
+ }
|
|
|
+ if (ip != null && ip.contains(",")) {
|
|
|
+ ip = ip.split(",")[0].trim();
|
|
|
+ }
|
|
|
+ return ip;
|
|
|
+ }
|
|
|
+}
|