Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
import org.springframework.cloud.gateway.filter.factory.RequestHeaderSizeGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.RequestHeaderToRequestUriGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterProperties;
import org.springframework.cloud.gateway.filter.factory.RequestSizeGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.RewriteLocationResponseHeaderGatewayFilterFactory;
Expand Down Expand Up @@ -213,7 +214,7 @@
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.enabled", matchIfMissing = true)
@EnableConfigurationProperties
@EnableConfigurationProperties(RequestRateLimiterProperties.class)
@AutoConfigureBefore({ HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class })
@AutoConfigureAfter({ GatewayReactiveLoadBalancerClientAutoConfiguration.class,
GatewayClassPathWarningAutoConfiguration.class })
Expand Down Expand Up @@ -677,8 +678,8 @@ public PrincipalNameKeyResolver principalNameKeyResolver() {
@ConditionalOnBean({ RateLimiter.class, KeyResolver.class })
@ConditionalOnEnabledFilter
public RequestRateLimiterGatewayFilterFactory requestRateLimiterGatewayFilterFactory(RateLimiter rateLimiter,
KeyResolver resolver) {
return new RequestRateLimiterGatewayFilterFactory(rateLimiter, resolver);
KeyResolver resolver, RequestRateLimiterProperties properties) {
return new RequestRateLimiterGatewayFilterFactory(rateLimiter, resolver, properties);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.data.redis.autoconfigure.DataRedisReactiveAutoConfiguration;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiterProperties;
import org.springframework.cloud.gateway.route.RedisRouteDefinitionRepository;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.support.ConfigurationService;
Expand All @@ -51,6 +53,7 @@
@ConditionalOnBean(ReactiveRedisTemplate.class)
@ConditionalOnClass({ RedisTemplate.class, DispatcherHandler.class })
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".redis.enabled", matchIfMissing = true)
@EnableConfigurationProperties(RedisRateLimiterProperties.class)
class GatewayRedisAutoConfiguration {

@Bean
Expand All @@ -67,8 +70,8 @@ public RedisScript redisRequestRateLimiterScript() {
@ConditionalOnMissingBean
public RedisRateLimiter redisRateLimiter(ReactiveStringRedisTemplate redisTemplate,
@Qualifier(RedisRateLimiter.REDIS_SCRIPT_NAME) RedisScript<List<Long>> redisScript,
ConfigurationService configurationService) {
return new RedisRateLimiter(redisTemplate, redisScript, configurationService);
ConfigurationService configurationService, RedisRateLimiterProperties redisRateLimiterProperties) {
return new RedisRateLimiter(redisTemplate, redisScript, configurationService, redisRateLimiterProperties);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
Expand All @@ -39,7 +38,6 @@
* User Request Rate Limiter filter. See https://stripe.com/blog/rate-limiters and
* https://gist.github.com/ptarjan/e38f45f2dfe601419ca3af937fff574d#file-1-check_request_rate_limiter-rb-L11-L34.
*/
@ConfigurationProperties("spring.cloud.gateway.server.webflux.filter.request-rate-limiter")
public class RequestRateLimiterGatewayFilterFactory
extends AbstractGatewayFilterFactory<RequestRateLimiterGatewayFilterFactory.Config> {

Expand All @@ -54,24 +52,18 @@ public class RequestRateLimiterGatewayFilterFactory

private final KeyResolver defaultKeyResolver;

/**
* Switch to deny requests if the Key Resolver returns an empty key, defaults to true.
*/
private boolean denyEmptyKey = true;

/** HttpStatus to return when denyEmptyKey is true, defaults to FORBIDDEN. */
private String emptyKeyStatusCode = HttpStatus.FORBIDDEN.name();

/**
* Switch to throw a {@link HttpClientErrorException} when the request is denied by
* the RateLimiter, defaults to false.
*/
private boolean throwOnLimit = false;
private final RequestRateLimiterProperties properties;

public RequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver) {
this(defaultRateLimiter, defaultKeyResolver, new RequestRateLimiterProperties());
}

public RequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver,
RequestRateLimiterProperties properties) {
super(Config.class);
this.defaultRateLimiter = defaultRateLimiter;
this.defaultKeyResolver = defaultKeyResolver;
this.properties = properties;
}

public KeyResolver getDefaultKeyResolver() {
Expand All @@ -82,39 +74,87 @@ public RateLimiter getDefaultRateLimiter() {
return defaultRateLimiter;
}

/**
* The externalized filter properties bound from configuration.
* @return the properties backing this filter factory
*/
public RequestRateLimiterProperties getProperties() {
return properties;
}

/**
* @return whether requests with an empty key are denied
* @deprecated in favor of {@link RequestRateLimiterProperties#isDenyEmptyKey()} via
* {@link #getProperties()}
*/
@Deprecated(since = "5.0.3")
public boolean isDenyEmptyKey() {
return denyEmptyKey;
return properties.isDenyEmptyKey();
}

/**
* @param denyEmptyKey whether requests with an empty key are denied
* @deprecated in favor of
* {@link RequestRateLimiterProperties#setDenyEmptyKey(boolean)} via
* {@link #getProperties()}
*/
@Deprecated(since = "5.0.3")
public void setDenyEmptyKey(boolean denyEmptyKey) {
this.denyEmptyKey = denyEmptyKey;
properties.setDenyEmptyKey(denyEmptyKey);
}

/**
* @return the status code returned when an empty key is denied
* @deprecated in favor of
* {@link RequestRateLimiterProperties#getEmptyKeyStatusCode()} via
* {@link #getProperties()}
*/
@Deprecated(since = "5.0.3")
public String getEmptyKeyStatusCode() {
return emptyKeyStatusCode;
return properties.getEmptyKeyStatusCode();
}

/**
* @param emptyKeyStatusCode the status code returned when an empty key is denied
* @deprecated in favor of
* {@link RequestRateLimiterProperties#setEmptyKeyStatusCode(String)} via
* {@link #getProperties()}
*/
@Deprecated(since = "5.0.3")
public void setEmptyKeyStatusCode(String emptyKeyStatusCode) {
this.emptyKeyStatusCode = emptyKeyStatusCode;
properties.setEmptyKeyStatusCode(emptyKeyStatusCode);
}

/**
* @return whether an exception is thrown when the request is rate limited
* @deprecated in favor of {@link RequestRateLimiterProperties#isThrowOnLimit()} via
* {@link #getProperties()}
*/
@Deprecated(since = "5.0.3")
public boolean isThrowOnLimit() {
return throwOnLimit;
return properties.isThrowOnLimit();
}

/**
* @param throwOnLimit whether an exception is thrown when the request is rate limited
* @deprecated in favor of
* {@link RequestRateLimiterProperties#setThrowOnLimit(boolean)} via
* {@link #getProperties()}
*/
@Deprecated(since = "5.0.3")
public void setThrowOnLimit(boolean throwOnLimit) {
this.throwOnLimit = throwOnLimit;
properties.setThrowOnLimit(throwOnLimit);
}

@SuppressWarnings("unchecked")
@Override
public GatewayFilter apply(Config config) {
KeyResolver resolver = getOrDefault(config.keyResolver, defaultKeyResolver);
RateLimiter<Object> limiter = getOrDefault(config.rateLimiter, defaultRateLimiter);
boolean denyEmpty = getOrDefault(config.denyEmptyKey, this.denyEmptyKey);
boolean denyEmpty = getOrDefault(config.denyEmptyKey, properties.isDenyEmptyKey());
HttpStatusHolder emptyKeyStatus = HttpStatusHolder
.parse(getOrDefault(config.emptyKeyStatus, this.emptyKeyStatusCode));
boolean throwLimit = getOrDefault(config.throwOnLimit, this.throwOnLimit);
.parse(getOrDefault(config.emptyKeyStatus, properties.getEmptyKeyStatusCode()));
boolean throwLimit = getOrDefault(config.throwOnLimit, properties.isThrowOnLimit());

return (exchange, chain) -> resolver.resolve(exchange).defaultIfEmpty(EMPTY_KEY).flatMap(key -> {
if (EMPTY_KEY.equals(key)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2013-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.gateway.filter.factory;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.style.ToStringCreator;
import org.springframework.http.HttpStatus;
import org.springframework.web.client.HttpClientErrorException;

/**
* Externalized configuration for {@link RequestRateLimiterGatewayFilterFactory}.
* Extracting the bound properties into a dedicated class with a default constructor lets
* Spring Cloud's {@code ConfigurationPropertiesRebinder} rebind them on a refresh
* (including resetting removed properties to their defaults), which is not possible while
* the properties live on the {@link RequestRateLimiterGatewayFilterFactory} bean whose
* {@code RateLimiter}/{@code KeyResolver} dependencies are constructor injected.
*
* @author Aryamann Singh
*/
@ConfigurationProperties("spring.cloud.gateway.server.webflux.filter.request-rate-limiter")
public class RequestRateLimiterProperties {

/**
* Switch to deny requests if the Key Resolver returns an empty key, defaults to true.
*/
private boolean denyEmptyKey = true;

/** HttpStatus to return when denyEmptyKey is true, defaults to FORBIDDEN. */
private String emptyKeyStatusCode = HttpStatus.FORBIDDEN.name();

/**
* Switch to throw a {@link HttpClientErrorException} when the request is denied by
* the RateLimiter, defaults to false.
*/
private boolean throwOnLimit = false;

public boolean isDenyEmptyKey() {
return denyEmptyKey;
}

public void setDenyEmptyKey(boolean denyEmptyKey) {
this.denyEmptyKey = denyEmptyKey;
}

public String getEmptyKeyStatusCode() {
return emptyKeyStatusCode;
}

public void setEmptyKeyStatusCode(String emptyKeyStatusCode) {
this.emptyKeyStatusCode = emptyKeyStatusCode;
}

public boolean isThrowOnLimit() {
return throwOnLimit;
}

public void setThrowOnLimit(boolean throwOnLimit) {
this.throwOnLimit = throwOnLimit;
}

@Override
public String toString() {
return new ToStringCreator(this).append("denyEmptyKey", denyEmptyKey)
.append("emptyKeyStatusCode", emptyKeyStatusCode)
.append("throwOnLimit", throwOnLimit)
.toString();
}

}
Loading