Spring Cloud Gateway 根据客户端 IP 限制请求速率
1、简介
在本教程中,我们将学习如何在 Spring Cloud Gateway 中根据客户端的实际 IP 地址来限制请求速率。
简而言之,我们将在路由上设置 RequestRateLimiter
Filter,然后配置网关根据 IP 地址来限制客户端的请求。
2、路由配置
首先,我们需要配置 Spring Cloud Gateway 以对特定路由进行速率限制。为此,我们将使用由 spring-boot-starter-data-redis-reactive
实现的经典 令牌桶(Token Bucket) Rate Limiter。简而言之, Rate Limiter 创建一个带有唯一 Key 的 Bucket,该 Key 用于标识 Bucket 自身,并具有固定的初始 Token 容量,随时间自动生成 Token。然后,对于每个请求,Rate Limiter 会获取其关联的 Bucket,并试图减少其 Bucket 中的一个 Token。如果 Bucket 中的 Token 数量不足,将拒绝传入的请求。
在分布式系统中,我们希望所有系统对同一客户端的请求都采用相同的限速策略。所以,我们使用分布式缓存 Redis 来存储 Bucket。在本例中,我们预先配置了一个 Redis 实例。
接下来,配置一个带有 Rate Limiter 的路由。监听 /example
端点,并将请求转发至 http://example.org
:
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route("requestratelimiter_route", p -> p
.path("/example")
.filters(f -> f.requestRateLimiter(r -> r.setRateLimiter(redisRateLimiter())))
.uri("http://example.org"))
.build();
}
上述代码中,我们使用 .setRateLimiter()
方法为路由配置了一个 RequestRateLimiter
。我们通过 redisRatelimiter()
方法定义了 RedisRateLimiter
Bean,以管理 Rate Limiter 的状态:
@Bean
public RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter(1, 1, 1);
}
如上,在配置速率限制时,将构造函数参数 replenishRate
、burstCapacity
和 requestedToken
属性都设置为 1
。这样就可以轻易地触发 /example
端点的限速,从而返回 HTTP 429 响应码。
3、KeyResolver
Bean
Rate Limiter 必须通过一个唯一 Key 来识别每个访问端点的客户端,从而找到与该 Key 关联的 Bucket。这种情况下,客户端的 IP 地址就是很好的选择
因此,之前配置的 RequestRateLimiter
需要使用一个 KeyResolver
Bean,以从请求中解析出 Key。
4、KeyResolver 解析客户端 IP 地址
目前,该接口还没有默认的实现,因必须自定义一个:
@Component
public class SimpleClientAddressResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Optional.ofNullable(exchange.getRequest().getRemoteAddress())
.map(InetSocketAddress::getAddress)
.map(InetAddress::getHostAddress)
.map(Mono::just)
.orElse(Mono.empty());
}
}
我们使用 ServerWebExchange
对象来获取客户端的 IP 地址。如果无法获取 IP 地址,将返回 Mono.empty()
,向 Rate Limiter 发出信号,默认情况下拒绝请求。不过,我们可以通过将 .setDenyEmptyKey()
设置为 false
来让 Rate Limiter 在 KeyResolver
返回 empty key 时允许请求。
通过向 .setKeyResolver()
方法提供自定义 KeyResolver
实现,为每个不同路由设置不同的 KeyResolver
:
builder.routes()
.route("ipaddress_route", p -> p
.path("/example2")
.filters(f -> f.requestRateLimiter(r -> r.setRateLimiter(redisRateLimiter())
.setDenyEmptyKey(false)
.setKeyResolver(new SimpleClientAddressResolver())))
.uri("http://example.org"))
.build();
4.1、代理服务器
如果 Spring Cloud Gateway 直接监听客户端请求,那么使用上述的实现方法就够了。
但是,如果我们在代理服务器后部署网关,此时所有主机地址都将相同(都成了网关地址)。因此,Rate Limiter 会将所有请求视为来自同一个客户端,并限制其请求速率。
为了解决这个问题,我们可以从 X-Forwarded-For
Header 来获取到请求代理服务器的实际源客户端 IP 地址。例如,配置 KeyResolver
,读取源 IP 地址:
@Primary
@Component
public class ProxiedClientAddressResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
XForwardedRemoteAddressResolver resolver = XForwardedRemoteAddressResolver.maxTrustedIndex(1);
InetSocketAddress inetSocketAddress = resolver.resolve(exchange);
return Mono.just(inetSocketAddress.getAddress().getHostAddress());
}
}
maxTrustedIndex(1)
表示网关前面只有一个代理服务器。此外,我们用 @Primary
对 KeyResolver
进行注解,使其在被注入时优先于之前的实现。
5、总结
在本文中,我们学习了如何在 Spring Cloud Gateway 中使用令牌桶算法根据客户端 IP 限制请求速率,以及如何通过 KeyResolver
解析真实的客户端 IP。
参考:https://www.baeldung.com/spring-cloud-gateway-rate-limit-by-client-ip