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);
}
举例来说,我们配置了速率限制,所有replenishRate
、burstCapacity
和requestedToken
属性都设置为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上找到。