Skip to content

Commit 956122e

Browse files
committed
feat: add api rate limiter
1 parent 01952d9 commit 956122e

File tree

7 files changed

+160
-25
lines changed

7 files changed

+160
-25
lines changed

src/main/java/tech/wetech/flexmodel/application/ApiRuntimeApplicationService.java

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@
1010
import lombok.extern.slf4j.Slf4j;
1111
import tech.wetech.flexmodel.codegen.entity.ApiInfo;
1212
import tech.wetech.flexmodel.codegen.entity.ApiLog;
13-
import tech.wetech.flexmodel.domain.model.api.ApiInfoService;
14-
import tech.wetech.flexmodel.domain.model.api.ApiLogService;
15-
import tech.wetech.flexmodel.domain.model.api.LogData;
16-
import tech.wetech.flexmodel.domain.model.api.LogLevel;
13+
import tech.wetech.flexmodel.domain.model.api.*;
1714
import tech.wetech.flexmodel.domain.model.data.DataService;
1815
import tech.wetech.flexmodel.domain.model.idp.IdentityProviderService;
1916
import tech.wetech.flexmodel.domain.model.modeling.ModelService;
17+
import tech.wetech.flexmodel.domain.model.settings.Settings;
18+
import tech.wetech.flexmodel.domain.model.settings.SettingsService;
2019
import tech.wetech.flexmodel.graphql.GraphQLProvider;
2120
import tech.wetech.flexmodel.util.JsonUtils;
2221
import tech.wetech.flexmodel.util.UriTemplate;
@@ -54,6 +53,8 @@ public class ApiRuntimeApplicationService {
5453

5554
@Inject
5655
GraphQLProvider graphQLProvider;
56+
@Inject
57+
SettingsService settingsService;
5758

5859
public ExecutionResult execute(String operationName, String query, Map<String, Object> variables) {
5960
GraphQL graphQL = graphQLProvider.getGraphQL();
@@ -78,6 +79,30 @@ public void accept(RoutingContext routingContext) {
7879
Map<String, String> pathParameters = uriTemplate.match(new UriTemplate(routingContext.normalizedPath()));
7980
String method = routingContext.request().method().name();
8081
if (pathParameters != null && method.equals(apiInfo.getMethod())) {
82+
Boolean rateLimitingEnabled = (Boolean) meta.getOrDefault("rateLimitingEnabled", false);
83+
Settings settings = settingsService.getSettings();
84+
if (rateLimitingEnabled || settings.getSecurity().isRateLimitingEnabled()) {
85+
int maxRequestCount = settings.getSecurity().getMaxRequestCount();
86+
int intervalInSeconds = settings.getSecurity().getIntervalInSeconds();
87+
ApiRateLimiterHolder.ApiRateLimiter apiRateLimiter;
88+
if (rateLimitingEnabled) {
89+
maxRequestCount = (int) meta.get("maxRequestCount");
90+
intervalInSeconds = (int) meta.get("intervalInSeconds");
91+
apiRateLimiter = ApiRateLimiterHolder.getApiRateLimiter(apiInfo.getMethod() + ":" + apiInfo.getPath(), maxRequestCount, intervalInSeconds);
92+
} else {
93+
apiRateLimiter = ApiRateLimiterHolder.getApiRateLimiter(apiInfo.getMethod() + ":" + apiInfo.getPath() + "@default", maxRequestCount, intervalInSeconds);
94+
}
95+
if (!apiRateLimiter.tryAcquire()) {
96+
Map<String, Object> result = new HashMap<>();
97+
result.put("messasge", "Too many requests.");
98+
result.put("code", -1);
99+
result.put("success", false);
100+
routingContext.response()
101+
.putHeader("Content-Type", "application/json")
102+
.end(JsonUtils.getInstance().stringify(result));
103+
return;
104+
}
105+
}
81106
log.debug("Matched request for api: {}", apiInfo);
82107
boolean isAuth = (boolean) meta.get("auth");
83108
if (isAuth) {
@@ -118,16 +143,6 @@ public void accept(RoutingContext routingContext) {
118143
.putHeader("Content-Type", "application/json")
119144
.end(JsonUtils.getInstance().stringify(result));
120145
}
121-
// switch (restAPIType) {
122-
// case "list" -> doList(routingContext, pathParameters, apiInfo);
123-
// case "view" -> doView(routingContext, pathParameters, apiInfo);
124-
// case "create" -> doCreate(routingContext, pathParameters, apiInfo);
125-
// case "update" -> doUpdate(routingContext, pathParameters, apiInfo);
126-
// case "delete" -> doDelete(routingContext, pathParameters, apiInfo);
127-
// default -> {
128-
// routingContext.response().end("Matched request for path: " + routingContext.normalisedPath());
129-
// }
130-
// }
131146
break;
132147
}
133148
}

src/main/java/tech/wetech/flexmodel/domain/model/api/ApiInfoService.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package tech.wetech.flexmodel.domain.model.api;
22

3-
import io.quarkus.cache.CacheInvalidate;
3+
import io.quarkus.cache.CacheInvalidateAll;
44
import io.quarkus.cache.CacheResult;
55
import jakarta.enterprise.context.ApplicationScoped;
66
import jakarta.inject.Inject;
@@ -22,32 +22,34 @@ public List<ApiInfo> findList() {
2222
return apiInfoRepository.findAll();
2323
}
2424

25-
@CacheInvalidate(cacheName = "apiInfoList")
25+
@CacheInvalidateAll(cacheName = "apiInfoList")
2626
public ApiInfo create(ApiInfo apiInfo) {
2727
if (apiInfo.getName() == null || apiInfo.getName().isEmpty()) {
2828
throw new ApiInfoException("API name must not be null");
2929
}
3030
return apiInfoRepository.save(apiInfo);
3131
}
3232

33-
@CacheInvalidate(cacheName = "apiInfoList")
33+
@CacheInvalidateAll(cacheName = "apiInfoList")
3434
public ApiInfo update(ApiInfo apiInfo) {
3535
ApiInfo older = apiInfoRepository.findById(apiInfo.getId());
3636
if (older == null) {
3737
return apiInfo;
3838
}
3939
apiInfo.setCreatedAt(older.getCreatedAt());
4040
apiInfo.setEnabled(older.getEnabled());
41+
ApiRateLimiterHolder.removeApiRateLimiter(apiInfo.getMethod() + ":" + apiInfo.getPath());
4142
return apiInfoRepository.save(apiInfo);
4243
}
4344

44-
@CacheInvalidate(cacheName = "apiInfoList")
45+
@CacheInvalidateAll(cacheName = "apiInfoList")
4546
public ApiInfo updateIgnoreNull(ApiInfo apiInfo) {
4647
apiInfoRepository.updateIgnoreNull(apiInfo.getId(), apiInfo);
48+
ApiRateLimiterHolder.removeApiRateLimiter(apiInfo.getMethod() + ":" + apiInfo.getPath());
4749
return apiInfo;
4850
}
4951

50-
@CacheInvalidate(cacheName = "apiInfoList")
52+
@CacheInvalidateAll(cacheName = "apiInfoList")
5153
public void delete(String id) {
5254
apiInfoRepository.delete(id);
5355
apiInfoRepository.deleteByParentId(id);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package tech.wetech.flexmodel.domain.model.api;
2+
3+
import lombok.Getter;
4+
5+
import java.util.Map;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
import java.util.concurrent.atomic.AtomicInteger;
8+
9+
10+
/**
11+
* @author cjbi
12+
*/
13+
public class ApiRateLimiterHolder {
14+
15+
@Getter
16+
private static final Map<String, ApiRateLimiter> map = new ConcurrentHashMap<>();
17+
18+
public static ApiRateLimiter getApiRateLimiter(String apiId, int maxRequestCount, int intervalInSeconds) {
19+
return map.computeIfAbsent(apiId, k -> new ApiRateLimiter(maxRequestCount, intervalInSeconds));
20+
}
21+
22+
public static void removeApiRateLimiter(String apiId) {
23+
map.remove(apiId);
24+
}
25+
26+
public static void reset() {
27+
map.clear();
28+
}
29+
30+
/**
31+
* @author cjbi
32+
*/
33+
public static class ApiRateLimiter {
34+
35+
private final int maxRequestCount;
36+
private final int intervalInSeconds;
37+
private final AtomicInteger requestCounter = new AtomicInteger(0);
38+
private long startTime;
39+
40+
public ApiRateLimiter(int maxRequestCount, int intervalInSeconds) {
41+
this.maxRequestCount = maxRequestCount;
42+
this.intervalInSeconds = intervalInSeconds;
43+
this.startTime = System.currentTimeMillis();
44+
}
45+
46+
public synchronized boolean tryAcquire() {
47+
long currentTime = System.currentTimeMillis();
48+
if ((currentTime - startTime) / 1000 > intervalInSeconds) {
49+
// 重置时间窗口和请求计数器
50+
startTime = currentTime;
51+
requestCounter.set(0);
52+
}
53+
54+
// 检查当前时间窗口内的请求数是否超过限制
55+
if (requestCounter.get() < maxRequestCount) {
56+
requestCounter.incrementAndGet();
57+
return true;
58+
}
59+
return false;
60+
}
61+
}
62+
63+
64+
}

src/main/java/tech/wetech/flexmodel/domain/model/settings/Settings.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ public static class Log {
3030
@Getter
3131
@Setter
3232
public static class Security {
33-
private boolean apiRateLimitingEnabled = false;
34-
private int maxRequests = 500;
35-
private int limitRefreshPeriod = 60;
33+
private boolean rateLimitingEnabled = false;
34+
private int maxRequestCount = 500;
35+
private int intervalInSeconds = 60;
3636
}
3737

3838
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package tech.wetech.flexmodel.domain.model.settings;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import lombok.ToString;
6+
7+
/**
8+
* @author cjbi
9+
*/
10+
@Getter
11+
@AllArgsConstructor
12+
@ToString
13+
public class SettingsChanged {
14+
private Settings settings;
15+
}

src/main/java/tech/wetech/flexmodel/domain/model/settings/SettingsService.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package tech.wetech.flexmodel.domain.model.settings;
22

3-
import io.quarkus.cache.CacheInvalidate;
3+
import io.quarkus.cache.CacheInvalidateAll;
44
import io.quarkus.cache.CacheResult;
5+
import io.vertx.mutiny.core.eventbus.EventBus;
56
import jakarta.enterprise.context.ApplicationScoped;
67
import jakarta.inject.Inject;
78

@@ -14,14 +15,21 @@ public class SettingsService {
1415
@Inject
1516
SettingsRepository settingsRepository;
1617

18+
@Inject
19+
EventBus eventBus;
20+
1721
@CacheResult(cacheName = "settings")
1822
public Settings getSettings() {
1923
return settingsRepository.getSettings();
2024
}
2125

22-
@CacheInvalidate(cacheName = "settings")
26+
@CacheInvalidateAll(cacheName = "settings")
2327
public Settings saveSettings(Settings settings) {
24-
return settingsRepository.saveSettings(settings);
28+
try {
29+
return settingsRepository.saveSettings(settings);
30+
} finally {
31+
eventBus.publish("settings-changed", new SettingsChanged(settings));
32+
}
2533
}
2634

2735
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package tech.wetech.flexmodel.infrastructrue;
2+
3+
import io.quarkus.vertx.ConsumeEvent;
4+
import jakarta.enterprise.context.ApplicationScoped;
5+
import tech.wetech.flexmodel.domain.model.api.ApiRateLimiterHolder;
6+
import tech.wetech.flexmodel.domain.model.settings.SettingsChanged;
7+
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
11+
/**
12+
* @author cjbi
13+
*/
14+
@ApplicationScoped
15+
public class SettingsEventConsumer {
16+
17+
18+
@ConsumeEvent("settings-changed") // 监听特定地址的事件
19+
public void consume(SettingsChanged event) {
20+
System.out.println("Received message: " + event.getSettings());
21+
// 处理事件
22+
List<String> invalidKeys = new ArrayList<>();
23+
ApiRateLimiterHolder.getMap().forEach((k, v) -> {
24+
if (k.endsWith("@default")) {
25+
invalidKeys.add(k);
26+
}
27+
});
28+
invalidKeys.forEach(ApiRateLimiterHolder::removeApiRateLimiter);
29+
}
30+
31+
}

0 commit comments

Comments
 (0)