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);
}

如上,在配置速率限制时,将构造函数参数 replenishRateburstCapacityrequestedToken 属性都设置为 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) 表示网关前面只有一个代理服务器。此外,我们用 @PrimaryKeyResolver 进行注解,使其在被注入时优先于之前的实现。

5、总结

在本文中,我们学习了如何在 Spring Cloud Gateway 中使用令牌桶算法根据客户端 IP 限制请求速率,以及如何通过 KeyResolver 解析真实的客户端 IP。


参考:https://www.baeldung.com/spring-cloud-gateway-rate-limit-by-client-ip