From 00dd95ca6db6997cc59e7ba759b7842dadd8100c Mon Sep 17 00:00:00 2001 From: John Niang Date: Wed, 28 Jun 2023 23:42:11 +0800 Subject: [PATCH] Add rate limiter for signing up (#4128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /area core /milestone 2.7.x #### What this PR does / why we need it: Add rate limiter for signing up. We only allow 3 registrations within 1 hour by default, despite registration failure. #### Special notes for your reviewer: 1. Start Halo and console. 2. Try to enable registration for public users. 3. Browse 4. Input duplicate username for 4 times and see the result. 5. Or input valid username for 4 times and see the result. #### Does this PR introduce a user-facing change? ```release-note 限制注册接口的请求速率 ``` --- .../app/theme/endpoint/PublicUserEndpoint.java | 17 ++++++++++++++++- application/src/main/resources/application.yaml | 5 ++++- .../theme/endpoint/PublicUserEndpointTest.java | 9 +++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/run/halo/app/theme/endpoint/PublicUserEndpoint.java b/application/src/main/java/run/halo/app/theme/endpoint/PublicUserEndpoint.java index b46117aa6..fa8f78700 100644 --- a/application/src/main/java/run/halo/app/theme/endpoint/PublicUserEndpoint.java +++ b/application/src/main/java/run/halo/app/theme/endpoint/PublicUserEndpoint.java @@ -4,6 +4,9 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder; +import io.github.resilience4j.ratelimiter.RateLimiterRegistry; +import io.github.resilience4j.ratelimiter.RequestNotPermitted; +import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; @@ -22,6 +25,8 @@ import run.halo.app.core.extension.User; import run.halo.app.core.extension.endpoint.CustomEndpoint; import run.halo.app.core.extension.service.UserService; import run.halo.app.extension.GroupVersion; +import run.halo.app.infra.exception.RateLimitExceededException; +import run.halo.app.infra.utils.IpAddressUtils; /** * User endpoint for unauthenticated user. @@ -35,6 +40,7 @@ public class PublicUserEndpoint implements CustomEndpoint { private final UserService userService; private final ServerSecurityContextRepository securityContextRepository; private final ReactiveUserDetailsService reactiveUserDetailsService; + private final RateLimiterRegistry rateLimiterRegistry; @Override public RouterFunction endpoint() { @@ -68,7 +74,16 @@ public class PublicUserEndpoint implements CustomEndpoint { .flatMap(user -> ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) .bodyValue(user) - ); + ) + .transformDeferred(getRateLimiterForSignUp(request.exchange())) + .onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new); + } + + private RateLimiterOperator getRateLimiterForSignUp(ServerWebExchange exchange) { + var clientIp = IpAddressUtils.getClientIp(exchange.getRequest()); + var rateLimiter = rateLimiterRegistry.rateLimiter("signup-from-ip-" + clientIp, + "signup"); + return RateLimiterOperator.of(rateLimiter); } private Mono authenticate(String username, ServerWebExchange exchange) { diff --git a/application/src/main/resources/application.yaml b/application/src/main/resources/application.yaml index be9b4c60d..56b063f34 100644 --- a/application/src/main/resources/application.yaml +++ b/application/src/main/resources/application.yaml @@ -81,4 +81,7 @@ resilience4j.ratelimiter: limitForPeriod: 10 limitRefreshPeriod: 1m timeoutDuration: 0s - + signup: + limitForPeriod: 3 + limitRefreshPeriod: 1h + timeoutDuration: 0s diff --git a/application/src/test/java/run/halo/app/theme/endpoint/PublicUserEndpointTest.java b/application/src/test/java/run/halo/app/theme/endpoint/PublicUserEndpointTest.java index d6506cdcf..46b4ba63b 100644 --- a/application/src/test/java/run/halo/app/theme/endpoint/PublicUserEndpointTest.java +++ b/application/src/test/java/run/halo/app/theme/endpoint/PublicUserEndpointTest.java @@ -6,6 +6,8 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.github.resilience4j.ratelimiter.RateLimiter; +import io.github.resilience4j.ratelimiter.RateLimiterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,6 +37,9 @@ class PublicUserEndpointTest { @Mock private ReactiveUserDetailsService reactiveUserDetailsService; + @Mock + RateLimiterRegistry rateLimiterRegistry; + @InjectMocks private PublicUserEndpoint publicUserEndpoint; @@ -63,8 +68,12 @@ class PublicUserEndpointTest { .authorities("test-role") .build())); + when(rateLimiterRegistry.rateLimiter("signup-from-ip-127.0.0.1", "signup")) + .thenReturn(RateLimiter.ofDefaults("signup")); + webClient.post() .uri("/users/-/signup") + .header("X-Forwarded-For", "127.0.0.1") .bodyValue(new PublicUserEndpoint.SignUpRequest(user, "fake-password")) .exchange() .expectStatus().isOk();