From 9379ff2d46a99013d6b54a5fa17b7698ec8d7e7b Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 5 Dec 2025 14:30:06 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=85=81=E8=AE=B8=E6=89=93=E5=BC=80?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E5=85=81=E8=AE=B8=E6=9C=AC=E5=9C=B0=E8=AE=BF=E9=97=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/main/config/application.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/config/application.properties b/server/src/main/config/application.properties index 54854c037..883b397da 100644 --- a/server/src/main/config/application.properties +++ b/server/src/main/config/application.properties @@ -99,7 +99,7 @@ base.url = ${KK_BASE_URL:default} # trust.host = * # # 当前配置: -trust.host = ${KK_TRUST_HOST:default} +trust.host = localhost # 不信任站点黑名单配置,多个用','隔开 # 黑名单优先级高于白名单,设置后将禁止预览来自这些站点的文件 @@ -183,7 +183,7 @@ watermark.angle = ${WATERMARK_ANGLE:10} #首页功能设置 #是否禁用首页文件上传 -file.upload.disable = ${KK_FILE_UPLOAD_DISABLE:true} +file.upload.disable = ${KK_FILE_UPLOAD_DISABLE:false} # 备案信息,默认为空 beian = ${KK_BEIAN:default} #禁止上传类型 From 9d324a23c7f931e59d0a62623951a790ce96bc83 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 5 Dec 2025 17:35:42 +0800 Subject: [PATCH 2/2] ai code --- .../cn/keking/config/ConfigConstants.java | 30 +++++++ .../keking/config/ConfigRefreshComponent.java | 6 ++ .../main/java/cn/keking/config/WebConfig.java | 11 +++ .../service/cache/MemoryRateLimiter.java | 62 +++++++++++++ .../cn/keking/service/cache/RateLimiter.java | 13 +++ .../service/cache/RateLimiterFactory.java | 19 ++++ .../web/filter/RateLimitInterceptor.java | 88 +++++++++++++++++++ 7 files changed, 229 insertions(+) create mode 100644 server/src/main/java/cn/keking/service/cache/MemoryRateLimiter.java create mode 100644 server/src/main/java/cn/keking/service/cache/RateLimiter.java create mode 100644 server/src/main/java/cn/keking/service/cache/RateLimiterFactory.java create mode 100644 server/src/main/java/cn/keking/web/filter/RateLimitInterceptor.java diff --git a/server/src/main/java/cn/keking/config/ConfigConstants.java b/server/src/main/java/cn/keking/config/ConfigConstants.java index 69fd600ae..8cc650b1a 100644 --- a/server/src/main/java/cn/keking/config/ConfigConstants.java +++ b/server/src/main/java/cn/keking/config/ConfigConstants.java @@ -71,6 +71,8 @@ public class ConfigConstants { private static int pdfTimeout80; private static int pdfTimeout200; private static int pdfThread; + private static int rateLimitInterval; + private static int rateLimitMaxRequests; public static final String DEFAULT_CACHE_ENABLED = "true"; public static final String DEFAULT_TXT_TYPE = "txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd,xbrl"; @@ -115,6 +117,8 @@ public class ConfigConstants { public static final String DEFAULT_PDF_TIMEOUT80 = "180"; public static final String DEFAULT_PDF_TIMEOUT200 = "300"; public static final String DEFAULT_PDF_THREAD = "5"; + public static final String DEFAULT_RATE_LIMIT_INTERVAL = "60"; + public static final String DEFAULT_RATE_LIMIT_MAX_REQUESTS = "100"; public static Boolean isCacheEnabled() { return cacheEnabled; @@ -648,6 +652,32 @@ public static void setPdfThreadValue(int pdfThread) { ConfigConstants.pdfThread = pdfThread; } + public static int getRateLimitInterval() { + return rateLimitInterval; + } + + @Value("${rate.limit.interval:60}") + public void setRateLimitInterval(String rateLimitInterval) { + setRateLimitIntervalValue(Integer.parseInt(rateLimitInterval)); + } + + public static void setRateLimitIntervalValue(int rateLimitInterval) { + ConfigConstants.rateLimitInterval = rateLimitInterval; + } + + public static int getRateLimitMaxRequests() { + return rateLimitMaxRequests; + } + + @Value("${rate.limit.max.requests:100}") + public void setRateLimitMaxRequests(String rateLimitMaxRequests) { + setRateLimitMaxRequestsValue(Integer.parseInt(rateLimitMaxRequests)); + } + + public static void setRateLimitMaxRequestsValue(int rateLimitMaxRequests) { + ConfigConstants.rateLimitMaxRequests = rateLimitMaxRequests; + } + /** * 以下为OFFICE转换模块设置 */ diff --git a/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java b/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java index d9a11f73b..0ef3c3d03 100644 --- a/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java +++ b/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java @@ -82,6 +82,8 @@ public void run() { int pdfTimeout80; int pdfTimeout200; int pdfThread; + int rateLimitInterval; + int rateLimitMaxRequests; while (true) { FileReader fileReader = new FileReader(configFilePath); BufferedReader bufferedReader = new BufferedReader(fileReader); @@ -134,6 +136,8 @@ public void run() { pdfTimeout80 = Integer.parseInt(properties.getProperty("pdf.timeout80", ConfigConstants.DEFAULT_PDF_TIMEOUT80)); pdfTimeout200 = Integer.parseInt(properties.getProperty("pdf.timeout200", ConfigConstants.DEFAULT_PDF_TIMEOUT200)); pdfThread = Integer.parseInt(properties.getProperty("pdf.thread", ConfigConstants.DEFAULT_PDF_THREAD)); + rateLimitInterval = Integer.parseInt(properties.getProperty("rate.limit.interval", ConfigConstants.DEFAULT_RATE_LIMIT_INTERVAL)); + rateLimitMaxRequests = Integer.parseInt(properties.getProperty("rate.limit.max.requests", ConfigConstants.DEFAULT_RATE_LIMIT_MAX_REQUESTS)); prohibitArray = prohibit.split(","); ConfigConstants.setCacheEnabledValueValue(cacheEnabled); @@ -181,6 +185,8 @@ public void run() { ConfigConstants.setPdfTimeout80Value(pdfTimeout80); ConfigConstants.setPdfTimeout200Value(pdfTimeout200); ConfigConstants.setPdfThreadValue(pdfThread); + ConfigConstants.setRateLimitIntervalValue(rateLimitInterval); + ConfigConstants.setRateLimitMaxRequestsValue(rateLimitMaxRequests); setWatermarkConfig(properties); bufferedReader.close(); fileReader.close(); diff --git a/server/src/main/java/cn/keking/config/WebConfig.java b/server/src/main/java/cn/keking/config/WebConfig.java index eb85367dc..1477d80b4 100644 --- a/server/src/main/java/cn/keking/config/WebConfig.java +++ b/server/src/main/java/cn/keking/config/WebConfig.java @@ -99,4 +99,15 @@ public FilterRegistrationBean getWatermarkConfigFilter() { registrationBean.setUrlPatterns(filterUri); return registrationBean; } + + @Bean + public FilterRegistrationBean getRateLimitInterceptor(RateLimitInterceptor rateLimitInterceptor) { + Set filterUri = new HashSet<>(); + filterUri.add("/onlinePreview"); + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(rateLimitInterceptor); + registrationBean.setUrlPatterns(filterUri); + registrationBean.setOrder(5); // 设置拦截器的执行顺序,确保在其他业务逻辑之前执行 + return registrationBean; + } } diff --git a/server/src/main/java/cn/keking/service/cache/MemoryRateLimiter.java b/server/src/main/java/cn/keking/service/cache/MemoryRateLimiter.java new file mode 100644 index 000000000..ae34a269c --- /dev/null +++ b/server/src/main/java/cn/keking/service/cache/MemoryRateLimiter.java @@ -0,0 +1,62 @@ +package cn.keking.service.cache; + +import cn.keking.config.ConfigConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 基于内存的限流器实现 + */ +public class MemoryRateLimiter implements RateLimiter { + private static final Logger LOGGER = LoggerFactory.getLogger(MemoryRateLimiter.class); + + private static class RateLimitInfo { + private final AtomicInteger count; + private final AtomicLong lastResetTime; + + RateLimitInfo() { + this.count = new AtomicInteger(0); + this.lastResetTime = new AtomicLong(System.currentTimeMillis()); + } + } + + private final ConcurrentMap rateLimitMap = new ConcurrentHashMap<>(); + + @Override + public boolean allowAccess(String key) { + try { + int interval = ConfigConstants.getRateLimitInterval() * 1000; // 转换为毫秒 + int maxRequests = ConfigConstants.getRateLimitMaxRequests(); + + RateLimitInfo info = rateLimitMap.computeIfAbsent(key, k -> new RateLimitInfo()); + + long now = System.currentTimeMillis(); + // 如果超过了时间间隔,重置计数 + if (now - info.lastResetTime.get() > interval) { + // 使用CAS操作确保只有一个线程能重置计数 + if (info.lastResetTime.compareAndSet(info.lastResetTime.get(), now)) { + info.count.set(0); + } + } + + // 如果计数超过了最大请求数,拒绝访问 + if (info.count.get() >= maxRequests) { + LOGGER.warn("Rate limit exceeded for key: {}", key); + return false; + } + + // 计数加1 + info.count.incrementAndGet(); + return true; + } catch (Exception e) { + LOGGER.error("Error in rate limiting", e); + // 异常时不限流 + return true; + } + } +} diff --git a/server/src/main/java/cn/keking/service/cache/RateLimiter.java b/server/src/main/java/cn/keking/service/cache/RateLimiter.java new file mode 100644 index 000000000..a9fc8f8f6 --- /dev/null +++ b/server/src/main/java/cn/keking/service/cache/RateLimiter.java @@ -0,0 +1,13 @@ +package cn.keking.service.cache; + +/** + * 限流器接口 + */ +public interface RateLimiter { + /** + * 检查是否允许访问 + * @param key 限流键,通常是IP地址 + * @return 是否允许访问 + */ + boolean allowAccess(String key); +} diff --git a/server/src/main/java/cn/keking/service/cache/RateLimiterFactory.java b/server/src/main/java/cn/keking/service/cache/RateLimiterFactory.java new file mode 100644 index 000000000..fba9ff3d6 --- /dev/null +++ b/server/src/main/java/cn/keking/service/cache/RateLimiterFactory.java @@ -0,0 +1,19 @@ +package cn.keking.service.cache; + +import org.springframework.stereotype.Component; + +/** + * 限流器工厂类 + */ +@Component +public class RateLimiterFactory { + /** + * 创建限流器实例 + * @return 限流器实例 + */ + public RateLimiter createRateLimiter() { + // 当前默认使用基于内存的限流器 + // 后续如果需要支持分布式,可以在这里修改为创建RedisRateLimiter等其他实现 + return new MemoryRateLimiter(); + } +} diff --git a/server/src/main/java/cn/keking/web/filter/RateLimitInterceptor.java b/server/src/main/java/cn/keking/web/filter/RateLimitInterceptor.java new file mode 100644 index 000000000..5d6570d96 --- /dev/null +++ b/server/src/main/java/cn/keking/web/filter/RateLimitInterceptor.java @@ -0,0 +1,88 @@ +package cn.keking.web.filter; + +import cn.keking.service.cache.RateLimiter; +import cn.keking.service.cache.RateLimiterFactory; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +/** + * 限流拦截器 + */ +@Component +public class RateLimitInterceptor implements Filter { + private static final Logger LOGGER = LoggerFactory.getLogger(RateLimitInterceptor.class); + + @Autowired + private RateLimiterFactory rateLimiterFactory; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + try { + // 获取客户端IP地址 + String clientIP = getClientIP(httpRequest); + LOGGER.debug("Request from IP: {}", clientIP); + + // 创建限流器实例 + RateLimiter rateLimiter = rateLimiterFactory.createRateLimiter(); + + // 检查是否允许访问 + if (!rateLimiter.allowAccess(clientIP)) { + httpResponse.setStatus(429); // 429 Too Many Requests + httpResponse.setContentType("text/plain;charset=UTF-8"); + httpResponse.getWriter().write("请求太频繁,请稍后再试"); + return; + } + + // 继续执行后续的过滤器或请求处理 + chain.doFilter(request, response); + } catch (Exception e) { + LOGGER.error("Error in rate limit interceptor", e); + // 异常时不限流,继续执行后续的过滤器或请求处理 + chain.doFilter(request, response); + } + } + + /** + * 获取客户端IP地址 + * @param request HTTP请求 + * @return 客户端IP地址 + */ + private String getClientIP(HttpServletRequest request) { + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + // 如果有多个IP地址,取第一个 + if (ip != null && ip.contains(",")) { + ip = ip.split(",")[0].trim(); + } + return ip; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // 初始化操作 + } + + @Override + public void destroy() { + // 销毁操作 + } +}