diff --git a/application/src/main/java/run/halo/app/theme/endpoint/CommentFinderEndpoint.java b/application/src/main/java/run/halo/app/theme/endpoint/CommentFinderEndpoint.java index 15a2845120..c4c63a223e 100644 --- a/application/src/main/java/run/halo/app/theme/endpoint/CommentFinderEndpoint.java +++ b/application/src/main/java/run/halo/app/theme/endpoint/CommentFinderEndpoint.java @@ -10,6 +10,8 @@ import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.github.resilience4j.ratelimiter.RateLimiterRegistry; +import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -20,6 +22,7 @@ import org.apache.commons.lang3.StringUtils; import org.springdoc.core.fn.builders.schema.Builder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; +import org.springframework.context.MessageSource; import org.springframework.data.domain.Sort; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; @@ -65,6 +68,8 @@ public class CommentFinderEndpoint implements CustomEndpoint { private final CommentService commentService; private final ReplyService replyService; private final SystemConfigurableEnvironmentFetcher environmentFetcher; + private final RateLimiterRegistry rateLimiterRegistry; + private final MessageSource messageSource; @Override public RouterFunction endpoint() { @@ -152,9 +157,17 @@ Mono createComment(ServerRequest request) { comment.getSpec().setUserAgent(HaloUtils.userAgentFrom(request)); return commentService.create(comment); }) + .transformDeferred(createIpBasedRateLimiter(request)) .flatMap(comment -> ServerResponse.ok().bodyValue(comment)); } + private RateLimiterOperator createIpBasedRateLimiter(ServerRequest request) { + var clientIp = IpAddressUtils.getIpAddress(request); + var rateLimiter = rateLimiterRegistry.rateLimiter("comment-creation-from-ip-" + clientIp, + "comment-creation"); + return RateLimiterOperator.of(rateLimiter); + } + Mono createReply(ServerRequest request) { String commentName = request.pathVariable("name"); return request.bodyToMono(ReplyRequest.class) diff --git a/application/src/main/resources/application.yaml b/application/src/main/resources/application.yaml index b301a8075f..da17dee024 100644 --- a/application/src/main/resources/application.yaml +++ b/application/src/main/resources/application.yaml @@ -73,3 +73,8 @@ resilience4j.ratelimiter: limitForPeriod: 3 limitRefreshPeriod: 1m timeoutDuration: 0 + comment-creation: + limitForPeriod: 10 + limitRefreshPeriod: 1m + timeoutDuration: 10s + diff --git a/application/src/test/java/run/halo/app/theme/endpoint/CommentFinderEndpointTest.java b/application/src/test/java/run/halo/app/theme/endpoint/CommentFinderEndpointTest.java index 598b4db9bb..daa3473a0a 100644 --- a/application/src/test/java/run/halo/app/theme/endpoint/CommentFinderEndpointTest.java +++ b/application/src/test/java/run/halo/app/theme/endpoint/CommentFinderEndpointTest.java @@ -3,12 +3,17 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.github.resilience4j.ratelimiter.RateLimiter; +import io.github.resilience4j.ratelimiter.RateLimiterConfig; +import io.github.resilience4j.ratelimiter.RateLimiterRegistry; +import java.time.Duration; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -54,6 +59,9 @@ class CommentFinderEndpointTest { @Mock private ReplyService replyService; + @Mock + private RateLimiterRegistry rateLimiterRegistry; + @InjectMocks private CommentFinderEndpoint commentFinderEndpoint; @@ -132,6 +140,15 @@ void listCommentReplies() { void createComment() { when(commentService.create(any())).thenReturn(Mono.empty()); + RateLimiterConfig config = RateLimiterConfig.custom() + .limitForPeriod(10) + .limitRefreshPeriod(Duration.ofSeconds(1)) + .timeoutDuration(Duration.ofSeconds(10)) + .build(); + RateLimiter rateLimiter = RateLimiter.of("comment-creation-from-ip-" + "0:0:0:0:0:0:0:0", + config); + when(rateLimiterRegistry.rateLimiter(anyString(), anyString())).thenReturn(rateLimiter); + final CommentRequest commentRequest = new CommentRequest(); Ref ref = new Ref(); ref.setGroup("content.halo.run");