Add rate limiter for signing up (#4128)

#### 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 <http://localhost:8090/console/login?type=signup>
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
限制注册接口的请求速率
```
pull/4146/head
John Niang 2023-06-28 23:42:11 +08:00 committed by GitHub
parent cabcd98ef4
commit 00dd95ca6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 29 additions and 2 deletions

View File

@ -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<ServerResponse> 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 <T> RateLimiterOperator<T> getRateLimiterForSignUp(ServerWebExchange exchange) {
var clientIp = IpAddressUtils.getClientIp(exchange.getRequest());
var rateLimiter = rateLimiterRegistry.rateLimiter("signup-from-ip-" + clientIp,
"signup");
return RateLimiterOperator.of(rateLimiter);
}
private Mono<Void> authenticate(String username, ServerWebExchange exchange) {

View File

@ -81,4 +81,7 @@ resilience4j.ratelimiter:
limitForPeriod: 10
limitRefreshPeriod: 1m
timeoutDuration: 0s
signup:
limitForPeriod: 3
limitRefreshPeriod: 1h
timeoutDuration: 0s

View File

@ -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();