mirror of https://github.com/halo-dev/halo
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
parent
cabcd98ef4
commit
00dd95ca6d
|
@ -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.apiresponse.Builder.responseBuilder;
|
||||||
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
|
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 io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
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.endpoint.CustomEndpoint;
|
||||||
import run.halo.app.core.extension.service.UserService;
|
import run.halo.app.core.extension.service.UserService;
|
||||||
import run.halo.app.extension.GroupVersion;
|
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.
|
* User endpoint for unauthenticated user.
|
||||||
|
@ -35,6 +40,7 @@ public class PublicUserEndpoint implements CustomEndpoint {
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final ServerSecurityContextRepository securityContextRepository;
|
private final ServerSecurityContextRepository securityContextRepository;
|
||||||
private final ReactiveUserDetailsService reactiveUserDetailsService;
|
private final ReactiveUserDetailsService reactiveUserDetailsService;
|
||||||
|
private final RateLimiterRegistry rateLimiterRegistry;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RouterFunction<ServerResponse> endpoint() {
|
public RouterFunction<ServerResponse> endpoint() {
|
||||||
|
@ -68,7 +74,16 @@ public class PublicUserEndpoint implements CustomEndpoint {
|
||||||
.flatMap(user -> ServerResponse.ok()
|
.flatMap(user -> ServerResponse.ok()
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.bodyValue(user)
|
.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) {
|
private Mono<Void> authenticate(String username, ServerWebExchange exchange) {
|
||||||
|
|
|
@ -81,4 +81,7 @@ resilience4j.ratelimiter:
|
||||||
limitForPeriod: 10
|
limitForPeriod: 10
|
||||||
limitRefreshPeriod: 1m
|
limitRefreshPeriod: 1m
|
||||||
timeoutDuration: 0s
|
timeoutDuration: 0s
|
||||||
|
signup:
|
||||||
|
limitForPeriod: 3
|
||||||
|
limitRefreshPeriod: 1h
|
||||||
|
timeoutDuration: 0s
|
||||||
|
|
|
@ -6,6 +6,8 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
@ -35,6 +37,9 @@ class PublicUserEndpointTest {
|
||||||
@Mock
|
@Mock
|
||||||
private ReactiveUserDetailsService reactiveUserDetailsService;
|
private ReactiveUserDetailsService reactiveUserDetailsService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
RateLimiterRegistry rateLimiterRegistry;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private PublicUserEndpoint publicUserEndpoint;
|
private PublicUserEndpoint publicUserEndpoint;
|
||||||
|
|
||||||
|
@ -63,8 +68,12 @@ class PublicUserEndpointTest {
|
||||||
.authorities("test-role")
|
.authorities("test-role")
|
||||||
.build()));
|
.build()));
|
||||||
|
|
||||||
|
when(rateLimiterRegistry.rateLimiter("signup-from-ip-127.0.0.1", "signup"))
|
||||||
|
.thenReturn(RateLimiter.ofDefaults("signup"));
|
||||||
|
|
||||||
webClient.post()
|
webClient.post()
|
||||||
.uri("/users/-/signup")
|
.uri("/users/-/signup")
|
||||||
|
.header("X-Forwarded-For", "127.0.0.1")
|
||||||
.bodyValue(new PublicUserEndpoint.SignUpRequest(user, "fake-password"))
|
.bodyValue(new PublicUserEndpoint.SignUpRequest(user, "fake-password"))
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isOk();
|
.expectStatus().isOk();
|
||||||
|
|
Loading…
Reference in New Issue