1. 概述

在这个快速教程中,我们将学习如何根据客户端的实际IP地址限制进入请求,针对的是我们的Spring Cloud Gateway

简单来说,我们将设置一个RequestRateLimiter过滤器到路由上,然后配置网关使用IP地址来限制每个独特客户端的请求。

2. 路由配置

首先,我们需要配置Spring Cloud Gateway对特定路由进行速率限制。为此,我们将使用由spring-boot-starter-data-redis-reactive实现的经典令牌桶速率限制器。简而言之,速率限制器会创建一个与标识符相关的桶,并且初始容量的令牌会在一段时间内逐渐补充。每次请求时,速率限制器都会检查相关桶并(如果可能)减少一个令牌,否则会拒绝请求。

由于我们在处理分布式系统,可能希望跟踪应用所有实例接收到的所有请求。因此,拥有分布式缓存系统来存储桶信息很有用。在这个例子中,我们已经预配置了一个Redis实例以模拟真实世界的应用场景。

接下来,我们将配置一个带有速率限制器的路由。我们将监听*/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:

@Bean
public RedisRateLimiter redisRateLimiter() {
    return new RedisRateLimiter(1, 1, 1);
}

举例来说,我们配置了速率限制,所有replenishRateburstCapacityrequestedToken属性都设置为1。这样可以轻松多次调用/example端点,并在请求过多时返回HTTP 429响应代码。

3. KeyResolver Bean

为了正常工作,速率限制器必须通过一个键识别每个访问端点的客户端。这个键标识着速率限制器将使用的桶,用于为每个请求消耗令牌。因此,我们希望键对于每个客户端都是唯一的。在这种情况下,我们将使用客户端的IP地址来监控他们的请求并在请求过多时进行限制。

因此,我们之前配置的RequestRateLimiter将使用一个KeyResolver bean,允许插件策略来确定限制请求的键。这意味着我们可以配置如何从每个请求中提取键。

4. 客户端IP地址在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(),向速率限制器信号并默认拒绝请求。然而,我们可以配置速率限制器在KeyResolver返回空键时允许请求,通过设置.setDenyEmptyKey()false。此外,我们也可以为每个不同的路由提供自定义的KeyResolver实现,通过.setKeyResolver()方法提供:

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. 代理后端的原始IP地址

如果Spring Cloud Gateway直接监听客户端请求,上述实现是有效的。但是,如果我们部署应用在代理之后,所有主机地址都将相同。因此,速率限制器会将所有请求视为来自同一客户端,并限制其能处理的请求数量。

为了解决这个问题,我们依赖于X-Forwarded-For标头来识别通过代理服务器连接的客户端的原始IP地址。例如,让我们配置KeyResolver以读取原始IP地址:

@Primary
@Component
public class ProxyClientAddressResolver 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());
    }
}

我们传递值1给maxTrustedIndex(),假设我们只有一个代理服务器。如果没有,值需要相应调整。此外,我们为这个KeyResolver添加了@Primary注解,使其优先于之前的实现。

5. 总结

在这篇文章中,我们基于客户端的IP地址配置了一个API速率限制器。首先,我们为一个带有令牌桶速率限制器的路由进行了配置。然后,我们探讨了KeyResolver如何确定每个请求使用的桶。最后,我们讨论了直接访问API或部署在代理后端时,通过KeyResolver分配客户端IP地址的不同策略。

这些示例的实现可以在GitHub上找到。