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
73 changes: 73 additions & 0 deletions RATE_LIMITER_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 限流器实现说明

## 概述

本项目实现了一个基于IP地址的简化版限流器,应用于文件预览系统的/onlinePreview接口,用于防止系统因同时访问的请求过多而导致资源不足。

## 实现步骤

### 1. 配置参数添加

在`ConfigConstants.java`中添加了以下限流参数:
- `rateLimitMaxRequests`:每个IP地址在指定时间窗口内的最大请求数,默认值为100
- `rateLimitTimeWindowSeconds`:时间窗口大小,单位为秒,默认值为60

### 2. 配置刷新支持

在`ConfigRefreshComponent.java`中添加了对限流参数的读取和更新支持,实现了配置的动态刷新。

### 3. 限流器核心逻辑

创建了以下核心类:
- `RateLimiter`:限流器接口,定义了`isAllowed`方法
- `InMemoryRateLimiter`:基于内存的限流器实现,使用`ConcurrentHashMap`存储IP地址的访问次数
- `RateLimiterFactory`:限流器工厂类,使用工厂模式创建不同类型的限流器实例

### 4. 拦截器实现

创建了`RateLimitInterceptor`拦截器,用于拦截/onlinePreview接口的请求,并使用限流器进行限流。

### 5. 拦截器注册

在`WebConfig.java`中注册了限流器拦截器,只拦截/onlinePreview接口的请求。

## 使用方法

### 1. 配置限流参数

在`config/application.properties`文件中添加以下配置参数:

```properties
# 每个IP地址在指定时间窗口内的最大请求数
rate.limit.max.requests=100
# 时间窗口大小,单位为秒
rate.limit.time.window.seconds=60
```

### 2. 重启应用

配置参数生效需要重启应用。

## 后续扩展

### 支持Redis限流器

如果需要支持分布式部署,可以添加Redis限流器实现:

1. 创建`RedisRateLimiter`类,实现`RateLimiter`接口
2. 在`RateLimiterFactory`中添加Redis限流器的创建逻辑
3. 配置Redis连接参数

### 支持其他类型的限流器

可以根据需要添加其他类型的限流器,如基于令牌桶算法的限流器等。

## 异常处理

限流器本身出现异常时,会自动允许请求,不会影响接口的功能。

## 性能考虑

- 使用`ConcurrentHashMap`存储IP地址的访问次数,保证线程安全
- 使用定时任务定期清理过期的IP记录,避免内存溢出
- 限流器的判断逻辑简单高效,不会对系统性能造成明显影响
7 changes: 7 additions & 0 deletions config/application.properties.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 限流器配置
# 每个IP地址在指定时间窗口内的最大请求数
rate.limit.max.requests=100
# 时间窗口大小,单位为秒
rate.limit.time.window.seconds=60

# 其他配置参数...
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
26 changes: 26 additions & 0 deletions server/src/main/java/cn/keking/config/ConfigConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,32 @@ public static void setPdfThreadValue(int pdfThread) {
ConfigConstants.pdfThread = pdfThread;
}

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;
}

public static int getRateLimitTimeWindowSeconds() {
return rateLimitTimeWindowSeconds;
}

@Value("${rate.limit.time.window.seconds:60}")
public void setRateLimitTimeWindowSeconds(String rateLimitTimeWindowSeconds) {
setRateLimitTimeWindowSecondsValue(Integer.parseInt(rateLimitTimeWindowSeconds));
}

public static void setRateLimitTimeWindowSecondsValue(int rateLimitTimeWindowSeconds) {
ConfigConstants.rateLimitTimeWindowSeconds = rateLimitTimeWindowSeconds;
}

/**
* 以下为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 rateLimitMaxRequests;
int rateLimitTimeWindowSeconds;
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));
rateLimitMaxRequests = Integer.parseInt(properties.getProperty("rate.limit.max.requests", ConfigConstants.DEFAULT_RATE_LIMIT_MAX_REQUESTS));
rateLimitTimeWindowSeconds = Integer.parseInt(properties.getProperty("rate.limit.time.window.seconds", ConfigConstants.DEFAULT_RATE_LIMIT_TIME_WINDOW_SECONDS));
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.setRateLimitMaxRequestsValue(rateLimitMaxRequests);
ConfigConstants.setRateLimitTimeWindowSecondsValue(rateLimitTimeWindowSeconds);
setWatermarkConfig(properties);
bufferedReader.close();
fileReader.close();
Expand Down
9 changes: 9 additions & 0 deletions server/src/main/java/cn/keking/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import cn.keking.interceptor.RateLimitInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

Expand All @@ -29,6 +31,13 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) {
LOGGER.info("Add resource locations: {}", filePath);
registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/","classpath:/resources/","classpath:/static/","classpath:/public/","file:" + filePath);
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册限流器拦截器,只拦截/onlinePreview接口
registry.addInterceptor(new RateLimitInterceptor())
.addPathPatterns("/onlinePreview");
}


@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package cn.keking.interceptor;

import cn.keking.rate.limiter.RateLimiter;
import cn.keking.rate.limiter.RateLimiterFactory;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;

import java.io.IOException;
import java.io.PrintWriter;

/**
* 基于IP地址的限流器拦截器
* @author kl
*/
public class RateLimitInterceptor implements HandlerInterceptor {

private static final Logger logger = LoggerFactory.getLogger(RateLimitInterceptor.class);

private static final String RATE_LIMIT_RESPONSE = "请求太频繁,请稍后再试";
private static final String CONTENT_TYPE = "text/plain;charset=UTF-8";

private final RateLimiter rateLimiter;

public RateLimitInterceptor() {
this.rateLimiter = RateLimiterFactory.getRateLimiter();
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
String ipAddress = getClientIpAddress(request);
logger.debug("收到请求,IP地址: {}", ipAddress);

if (rateLimiter.isAllowed(ipAddress)) {
logger.debug("IP地址: {} 请求允许", ipAddress);
return true;
} else {
logger.warn("IP地址: {} 请求被限流", ipAddress);
handleRateLimit(response);
return false;
}
} catch (Exception e) {
logger.error("限流器拦截器处理请求时发生异常,将允许请求", e);
// 限流器本身出现异常时,不能影响接口的功能,即异常时不限流
return true;
}
}

/**
* 获取客户端IP地址
* @param request HttpServletRequest
* @return 客户端IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 如果是多个IP地址,取第一个
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}

/**
* 处理限流请求,返回提示信息
* @param response HttpServletResponse
* @throws IOException IOException
*/
private void handleRateLimit(HttpServletResponse response) throws IOException {
response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
response.setContentType(CONTENT_TYPE);
PrintWriter writer = response.getWriter();
writer.write(RATE_LIMIT_RESPONSE);
writer.flush();
writer.close();
}
}
15 changes: 15 additions & 0 deletions server/src/main/java/cn/keking/rate/limiter/RateLimiter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package cn.keking.rate.limiter;

/**
* 限流器接口
* @author kl
*/
public interface RateLimiter {

/**
* 判断是否允许请求
* @param key 请求标识,这里使用IP地址
* @return 是否允许请求
*/
boolean isAllowed(String key);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package cn.keking.rate.limiter;

import cn.keking.config.ConfigConstants;
import cn.keking.rate.limiter.impl.InMemoryRateLimiter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* 限流器工厂类,使用工厂模式创建不同类型的限流器实例
* @author kl
*/
public class RateLimiterFactory {

private static final Logger logger = LoggerFactory.getLogger(RateLimiterFactory.class);

private static final String TYPE_IN_MEMORY = "inMemory";
private static volatile RateLimiter instance;

/**
* 获取限流器实例
* @return 限流器实例
*/
public static RateLimiter getRateLimiter() {
return getRateLimiter(TYPE_IN_MEMORY);
}

/**
* 根据类型获取限流器实例
* @param type 限流器类型
* @return 限流器实例
*/
public static RateLimiter getRateLimiter(String type) {
if (instance == null) {
synchronized (RateLimiterFactory.class) {
if (instance == null) {
instance = createRateLimiter(type);
}
}
}
return instance;
}

/**
* 创建限流器实例
* @param type 限流器类型
* @return 限流器实例
*/
private static RateLimiter createRateLimiter(String type) {
int maxRequests = ConfigConstants.getRateLimitMaxRequests();
int timeWindowSeconds = ConfigConstants.getRateLimitTimeWindowSeconds();

logger.info("创建限流器实例,类型: {}, 最大请求数: {}, 时间窗口: {}秒", type, maxRequests, timeWindowSeconds);

switch (type) {
case TYPE_IN_MEMORY:
return new InMemoryRateLimiter(maxRequests, timeWindowSeconds);
// 后续可以添加其他类型的限流器,如Redis限流器
// case TYPE_REDIS:
// return new RedisRateLimiter(maxRequests, timeWindowSeconds);
default:
logger.warn("未知的限流器类型: {}, 将使用默认的内存限流器", type);
return new InMemoryRateLimiter(maxRequests, timeWindowSeconds);
}
}
}
Loading