Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions server/src/main/config/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ base.url = ${KK_BASE_URL:default}
# trust.host = *
#
# 当前配置:
trust.host = ${KK_TRUST_HOST:default}
trust.host = localhost

# 不信任站点黑名单配置,多个用','隔开
# 黑名单优先级高于白名单,设置后将禁止预览来自这些站点的文件
Expand Down Expand Up @@ -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}
#禁止上传类型
Expand Down
30 changes: 30 additions & 0 deletions server/src/main/java/cn/keking/config/ConfigConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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转换模块设置
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
11 changes: 11 additions & 0 deletions server/src/main/java/cn/keking/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,15 @@ public FilterRegistrationBean<AttributeSetFilter> getWatermarkConfigFilter() {
registrationBean.setUrlPatterns(filterUri);
return registrationBean;
}

@Bean
public FilterRegistrationBean<RateLimitInterceptor> getRateLimitInterceptor(RateLimitInterceptor rateLimitInterceptor) {
Set<String> filterUri = new HashSet<>();
filterUri.add("/onlinePreview");
FilterRegistrationBean<RateLimitInterceptor> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(rateLimitInterceptor);
registrationBean.setUrlPatterns(filterUri);
registrationBean.setOrder(5); // 设置拦截器的执行顺序,确保在其他业务逻辑之前执行
return registrationBean;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, RateLimitInfo> 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;
}
}
}
13 changes: 13 additions & 0 deletions server/src/main/java/cn/keking/service/cache/RateLimiter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cn.keking.service.cache;

/**
* 限流器接口
*/
public interface RateLimiter {
/**
* 检查是否允许访问
* @param key 限流键,通常是IP地址
* @return 是否允许访问
*/
boolean allowAccess(String key);
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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() {
// 销毁操作
}
}