Rate Limiting a Spring API Using Bucket4j – 使用Bucket4j对Spring API进行速率限制

最后修改: 2020年 5月 27日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll focus on how to use Bucket4j to rate limit a Spring REST API.

在本教程中,我们将重点介绍如何使用Bucket4j来限制Spring REST API的速率

We’ll explore API rate limiting, learn about Bucket4j, and then work through a few ways of rate-limiting REST APIs in a Spring application.

我们将探讨API速率限制,了解Bucket4j,然后通过一些方法在Spring应用程序中对REST API进行速率限制。

2. API Rate Limiting

2.API速率限制

Rate limiting is a strategy to limit access to APIs. It restricts the number of API calls that a client can make within a certain time frame. This helps defend the API against overuse, both unintentional and malicious.

速率限制是一种策略,用于限制对API的访问。它限制了客户在一定时间内可以进行的API调用的数量。这有助于防止API被过度使用,包括无意的和恶意的。

Rate limits are often applied to an API by tracking the IP address, or in a more business-specific way, such as API keys or access tokens. As API developers, we have several options when a client reaches the limit:

速率限制通常是通过跟踪IP地址来应用于API,或以更具体的业务方式,如API密钥或访问令牌。作为API开发者,当客户达到限制时,我们有几种选择。

  • Queueing the request until the remaining time period has elapsed
  • Allowing the request immediately, but charging extra for this request
  • Rejecting the request (HTTP 429 Too Many Requests)

3. Bucket4j Rate Limiting Library

3.Bucket4j速率限制库

3.1. What Is Bucket4j?

3.1.什么是Bucket4j?

Bucket4j is a Java rate-limiting library based on the token-bucket algorithm. Bucket4j is a thread-safe library that can be used in either a standalone JVM application, or a clustered environment. It also supports in-memory or distributed caching via the JCache (JSR107) specification.

Bucket4j是一个基于token-bucket算法的Java速率限制库。Bucket4j 是一个线程安全的库,既可用于独立的 JVM 应用程序,也可用于集群环境。它还支持通过JCache (JSR107)规范进行内存内或分布式缓存。

3.2. Token-bucket Algorithm

3.2.代币桶算法

Let’s look at the algorithm intuitively in the context of API rate limiting.

让我们在API速率限制的背景下直观地看一下这个算法。

Say we have a bucket whose capacity is defined as the number of tokens that it can hold. Whenever a consumer wants to access an API endpoint, it must get a token from the bucket. We remove a token from the bucket if it’s available and accept the request. Conversely, we reject a request if the bucket doesn’t have any tokens.

假设我们有一个桶,其容量被定义为它可以容纳的令牌数量。每当消费者想要访问一个API端点时,它必须从桶中获取一个令牌。如果一个令牌是可用的,我们就从桶中移除该令牌,并接受该请求。反之,如果桶中没有任何令牌,我们就拒绝请求。

As requests are consuming tokens, we’re also replenishing them at some fixed rate, such that we never exceed the capacity of the bucket.

在请求消耗代币的同时,我们也在以某种固定的速度补充它们,这样我们就不会超过桶的容量。

Let’s consider an API that has a rate limit of 100 requests per minute. We can create a bucket with a capacity of 100, and a refill rate of 100 tokens per minute.

让我们考虑一个速率限制为每分钟100个请求的API。我们可以创建一个容量为100的桶,每分钟补给率为100个代币。

If we receive 70 requests, which is fewer than the available tokens in a given minute, we would add only 30 more tokens at the start of the next minute to bring the bucket up to capacity. On the other hand, if we exhaust all the tokens in 40 seconds, we would wait for 20 seconds to refill the bucket.

如果我们收到70个请求,少于某一分钟内的可用代币,我们将在下一分钟开始时只增加30个代币,以使桶的容量增加。另一方面,如果我们在40秒内用完了所有的代币,我们将等待20秒来重新填满这个桶。

4. Getting Started With Bucket4j

4.开始使用Bucket4j

4.1. Maven Configuration

4.1.Maven配置

Let’s begin by adding the bucket4j dependency to our pom.xml:

让我们首先将bucket4j依赖性添加到我们的pom.xml

<dependency>
    <groupId>com.github.vladimir-bukhtoyarov</groupId>
    <artifactId>bucket4j-core</artifactId>
    <version>4.10.0</version>
</dependency>

4.2. Terminology

4.2 术语

Before we look at how to use Bucket4j, we’ll briefly discuss some of the core classes, and how they represent the different elements in the formal model of the token-bucket algorithm.

在我们研究如何使用Bucket4j之前,我们将简要地讨论一些核心类,以及它们如何在令牌桶算法的正式模型中代表不同的元素。

The Bucket interface represents the token bucket with a maximum capacity. It provides methods such as tryConsume and tryConsumeAndReturnRemaining for consuming tokens. These methods return the result of consumption as true if the request conforms with the limits, and the token was consumed.

Bucket接口表示具有最大容量的代币桶。它提供了诸如tryConsumetryConsumeAndReturnRemaining的方法,用于消耗代币。如果请求符合限制,并且令牌被消耗,这些方法将消耗的结果返回为true

The Bandwidth class is the key building block of a bucket, as it defines the limits of the bucket. We use Bandwidth to configure the capacity of the bucket and the rate of refill.

Bandwidth类是水桶的关键构建模块,因为它定义了水桶的极限。我们使用Bandwidth来配置水桶的容量和补水率。

The Refill class is used to define the fixed rate at which tokens are added to the bucket. We can configure the rate as the number of tokens that would be added in a given time period. For example, 10 buckets per second or 200 tokens per 5 minutes, and so on.

Refill类用于定义将代币添加到桶中的固定比率。我们可以将速率配置为在给定时间段内增加的代币数量。例如,每秒10个桶或每5分钟200个代币,以此类推。

The tryConsumeAndReturnRemaining method in Bucket returns ConsumptionProbe. ConsumptionProbe contains, along with the result of consumption, the status of the bucket, such as the tokens remaining, or the time remaining until the requested tokens are available in the bucket again.

Bucket中的tryConsumeAndReturnRemaining方法返回ConsumptionProbeConsumptionProbe与消费的结果一起包含了桶的状态,例如剩余的代币,或者直到所请求的代币再次出现在桶中的剩余时间。

4.3. Basic Usage

4.3.基本用法

Let’s test some basic rate limit patterns.

让我们来测试一些基本的速率限制模式。

For a rate limit of 10 requests per minute, we’ll create a bucket with capacity 10 and a refill rate of 10 tokens per minute:

对于每分钟10个请求的速率限制,我们将创建一个容量为10的桶,每分钟补给率为10个代币。

Refill refill = Refill.intervally(10, Duration.ofMinutes(1));
Bandwidth limit = Bandwidth.classic(10, refill);
Bucket bucket = Bucket4j.builder()
    .addLimit(limit)
    .build();

for (int i = 1; i <= 10; i++) {
    assertTrue(bucket.tryConsume(1));
}
assertFalse(bucket.tryConsume(1));

Refill.intervally refills the bucket at the beginning of the time window, which in this case is 10 tokens at the start of the minute.

Refill.intervally在时间窗口开始时给水桶加水,在本例中是在一分钟开始时加10个代币。

Next, let’s see refill in action.

接下来,让我们看看笔芯的作用。

We’ll set a refill rate of 1 token per 2 seconds, and throttle our requests to honor the rate limit:

我们将设置一个每2秒1个代币的补给率,并节制我们的请求以遵守该率限制

Bandwidth limit = Bandwidth.classic(1, Refill.intervally(1, Duration.ofSeconds(2)));
Bucket bucket = Bucket4j.builder()
    .addLimit(limit)
    .build();
assertTrue(bucket.tryConsume(1));     // first request
Executors.newScheduledThreadPool(1)   // schedule another request for 2 seconds later
    .schedule(() -> assertTrue(bucket.tryConsume(1)), 2, TimeUnit.SECONDS); 

Suppose we have a rate limit of 10 requests per minute. At the same time, we may wish to avoid spikes that would exhaust all the tokens in the first 5 seconds. Bucket4j allows us to set multiple limits (Bandwidth) on the same bucket. Let’s add another limit that allows only 5 requests in a 20-second time window:

假设我们有一个每分钟10个请求的速率限制。同时,我们可能希望避免在前5秒内耗尽所有令牌的峰值。Bucket4j允许我们对同一个桶设置多个限制(Bandwidth)。让我们添加另一个限制,在20秒的时间窗口内只允许5个请求。

Bucket bucket = Bucket4j.builder()
    .addLimit(Bandwidth.classic(10, Refill.intervally(10, Duration.ofMinutes(1))))
    .addLimit(Bandwidth.classic(5, Refill.intervally(5, Duration.ofSeconds(20))))
    .build();

for (int i = 1; i <= 5; i++) {
    assertTrue(bucket.tryConsume(1));
}
assertFalse(bucket.tryConsume(1));

5. Rate Limiting a Spring API Using Bucket4j

5.使用Bucket4j对Spring API进行速率限制

Let’s use Bucket4j to apply a rate limit in a Spring REST API.

让我们使用Bucket4j在Spring REST API中应用一个速率限制。

5.1. Area Calculator API

5.1. 面积计算器API

We’ll implement a simple, but extremely popular, area calculator REST API. Currently, it calculates and returns the area of a rectangle given its dimensions:

我们将实现一个简单但非常流行的面积计算器REST API。目前,它计算并返回一个矩形的面积,给定其尺寸。

@RestController
class AreaCalculationController {

    @PostMapping(value = "/api/v1/area/rectangle")
    public ResponseEntity<AreaV1> rectangle(@RequestBody RectangleDimensionsV1 dimensions) {
        return ResponseEntity.ok(new AreaV1("rectangle", dimensions.getLength() * dimensions.getWidth()));
    }
}

Let’s ensure that our API is up and running:

让我们确保我们的API已经启动并运行。

$ curl -X POST http://localhost:9001/api/v1/area/rectangle \
    -H "Content-Type: application/json" \
    -d '{ "length": 10, "width": 12 }'

{ "shape":"rectangle","area":120.0 }

5.2. Applying Rate Limit

5.2.适用费率限制

Now we’ll introduce a naive rate limit, allowing the API 20 requests per minute. In other words, the API rejects a request if it’s already received 20 requests in a time window of 1 minute.

现在我们将引入一个天真的速率限制,允许API每分钟20个请求。换句话说,如果API在1分钟的时间窗口内已经收到了20个请求,它就会拒绝一个请求。

Let’s modify our Controller to create a Bucket and add the limit (Bandwidth):

让我们修改我们的控制器,创建一个Bucket,并添加限制(带宽):

@RestController
class AreaCalculationController {

    private final Bucket bucket;

    public AreaCalculationController() {
        Bandwidth limit = Bandwidth.classic(20, Refill.greedy(20, Duration.ofMinutes(1)));
        this.bucket = Bucket4j.builder()
            .addLimit(limit)
            .build();
    }
    //..
}

In this API, we can check whether the request is allowed by consuming a token from the bucket using the method tryConsume. If we’ve reached the limit, we can reject the request by responding with an HTTP 429 Too Many Requests status:

在这个API中,我们可以通过使用方法tryConsume从桶中消耗一个令牌来检查请求是否被允许。如果我们已经达到了限制,我们可以通过响应HTTP 429 Too Many Requests状态来拒绝该请求。

public ResponseEntity<AreaV1> rectangle(@RequestBody RectangleDimensionsV1 dimensions) {
    if (bucket.tryConsume(1)) {
        return ResponseEntity.ok(new AreaV1("rectangle", dimensions.getLength() * dimensions.getWidth()));
    }

    return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
}
# 21st request within 1 minute
$ curl -v -X POST http://localhost:9001/api/v1/area/rectangle \
    -H "Content-Type: application/json" \
    -d '{ "length": 10, "width": 12 }'

< HTTP/1.1 429

5.3. API Clients and Pricing Plan

5.3.API客户和定价计划

Now we have a naive rate limit that can throttle the API requests. Next, we’ll introduce pricing plans for more business-centered rate limits.

现在我们有一个天真的速率限制,可以节制API请求。接下来,我们将为更多以业务为中心的速率限制引入定价计划。

Pricing plans help us monetize our API. Let’s assume that we have the following plans for our API clients:

定价计划帮助我们实现API的货币化。让我们假设,我们的API客户有以下计划。

  • Free: 20 requests per hour per API client
  • Basic: 40 requests per hour per API client
  • Professional: 100 requests per hour per API client

Each API client gets a unique API key that they must send along with each request. This helps us identify the pricing plan linked with the API client.

每个API客户都有一个独特的API密钥,他们必须在每次请求时发送。这有助于我们识别与该API客户相关的定价计划。

Let’s define the rate limit (Bandwidth) for each pricing plan:

让我们定义每个定价计划的速率限制(带宽)。

enum PricingPlan {
    FREE {
        Bandwidth getLimit() {
            return Bandwidth.classic(20, Refill.intervally(20, Duration.ofHours(1)));
        }
    },
    BASIC {
        Bandwidth getLimit() {
            return Bandwidth.classic(40, Refill.intervally(40, Duration.ofHours(1)));
        }
    },
    PROFESSIONAL {
        Bandwidth getLimit() {
            return Bandwidth.classic(100, Refill.intervally(100, Duration.ofHours(1)));
        }
    };
    //..
}

Then let’s add a method to resolve the pricing plan from the given API key:

然后,让我们添加一个方法来解决来自给定API密钥的定价计划。

enum PricingPlan {
    
    static PricingPlan resolvePlanFromApiKey(String apiKey) {
        if (apiKey == null || apiKey.isEmpty()) {
            return FREE;
        } else if (apiKey.startsWith("PX001-")) {
            return PROFESSIONAL;
        } else if (apiKey.startsWith("BX001-")) {
            return BASIC;
        }
        return FREE;
    }
    //..
}

Next, we need to store the Bucket for each API key, and retrieve the Bucket for rate limiting:

接下来,我们需要为每个API密钥存储Bucket,并为速率限制检索Bucket

class PricingPlanService {

    private final Map<String, Bucket> cache = new ConcurrentHashMap<>();

    public Bucket resolveBucket(String apiKey) {
        return cache.computeIfAbsent(apiKey, this::newBucket);
    }

    private Bucket newBucket(String apiKey) {
        PricingPlan pricingPlan = PricingPlan.resolvePlanFromApiKey(apiKey);
        return Bucket4j.builder()
            .addLimit(pricingPlan.getLimit())
            .build();
    }
}

Now we have an in-memory store of buckets per API key. Let’s modify our Controller to use the PricingPlanService:

现在我们有一个每个API密钥的桶的内存存储。让我们修改我们的Controller以使用PricingPlanService

@RestController
class AreaCalculationController {

    private PricingPlanService pricingPlanService;

    public ResponseEntity<AreaV1> rectangle(@RequestHeader(value = "X-api-key") String apiKey,
        @RequestBody RectangleDimensionsV1 dimensions) {

        Bucket bucket = pricingPlanService.resolveBucket(apiKey);
        ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
        if (probe.isConsumed()) {
            return ResponseEntity.ok()
                .header("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens()))
                .body(new AreaV1("rectangle", dimensions.getLength() * dimensions.getWidth()));
        }
        
        long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
        return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
            .header("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill))
            .build();
    }
}

Let’s walk through the changes. The API client sends the API key with the X-api-key request header. We use the PricingPlanService to get the bucket for this API key, and check whether the request is allowed by consuming a token from the bucket.

让我们来看看这些变化。API客户端用X-api-key请求头来发送API密钥。我们使用PricingPlanService来获取该API密钥的桶,并通过消耗桶中的令牌来检查该请求是否被允许。

In order to enhance the client experience of the API, we’ll use the following additional response headers to send information about the rate limit:

为了增强客户端对API的体验,我们将使用以下额外的响应头来发送有关速率限制的信息。

  • X-Rate-Limit-Remaining: number of tokens remaining in the current time window
  • X-Rate-Limit-Retry-After-Seconds: remaining time, in seconds, until the bucket is refilled

We can call the ConsumptionProbe methods getRemainingTokens and getNanosToWaitForRefill to get the count of remaining tokens in the bucket and the time remaining until the next refill, respectively. The getNanosToWaitForRefill method returns 0 if we’re able to consume the token successfully.

我们可以调用ConsumptionProbe方法getRemainingTokensgetNanosToWaitForRefill来分别获得桶中剩余代币的数量和距离下次补货的剩余时间。getNanosToWaitForRefill方法返回0,如果我们能够成功消耗令牌。

Let’s call the API:

我们来调用API。

## successful request
$ curl -v -X POST http://localhost:9001/api/v1/area/rectangle \
    -H "Content-Type: application/json" -H "X-api-key:FX001-99999" \
    -d '{ "length": 10, "width": 12 }'

< HTTP/1.1 200
< X-Rate-Limit-Remaining: 11
{"shape":"rectangle","area":120.0}

## rejected request
$ curl -v -X POST http://localhost:9001/api/v1/area/rectangle \
    -H "Content-Type: application/json" -H "X-api-key:FX001-99999" \
    -d '{ "length": 10, "width": 12 }'

< HTTP/1.1 429
< X-Rate-Limit-Retry-After-Seconds: 583

5.4. Using Spring MVC Interceptor

5.4.使用Spring MVC拦截器

Suppose we now have to add a new API endpoint that calculates and returns the area of a triangle given its height and base:

假设我们现在要添加一个新的API端点,计算并返回一个三角形的面积,给定其高度和底。

@PostMapping(value = "/triangle")
public ResponseEntity<AreaV1> triangle(@RequestBody TriangleDimensionsV1 dimensions) {
    return ResponseEntity.ok(new AreaV1("triangle", 0.5d * dimensions.getHeight() * dimensions.getBase()));
}

As it turns out, we need to rate-limit our new endpoint as well. We can simply copy and paste the rate limit code from our previous endpoint. Alternatively, we can use Spring MVC’s HandlerInterceptor to decouple the rate limit code from the business code.

事实证明,我们也需要对我们的新端点进行速率限制。我们可以简单地复制和粘贴之前端点的速率限制代码。或者,我们可以使用Spring MVC的HandlerInterceptor来将速率限制代码与业务代码解耦

Let’s create a RateLimitInterceptor and implement the rate limit code in the preHandle method:

让我们创建一个RateLimitInterceptor并在preHandle方法中实现速率限制代码。

public class RateLimitInterceptor implements HandlerInterceptor {

    private PricingPlanService pricingPlanService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
      throws Exception {
        String apiKey = request.getHeader("X-api-key");
        if (apiKey == null || apiKey.isEmpty()) {
            response.sendError(HttpStatus.BAD_REQUEST.value(), "Missing Header: X-api-key");
            return false;
        }

        Bucket tokenBucket = pricingPlanService.resolveBucket(apiKey);
        ConsumptionProbe probe = tokenBucket.tryConsumeAndReturnRemaining(1);
        if (probe.isConsumed()) {
            response.addHeader("X-Rate-Limit-Remaining", String.valueOf(probe.getRemainingTokens()));
            return true;
        } else {
            long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
            response.addHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
            response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(),
              "You have exhausted your API Request Quota"); 
            return false;
        }
    }
}

Finally, we must add the interceptor to the InterceptorRegistry:

最后,我们必须把拦截器添加到InterceptorRegistry

public class AppConfig implements WebMvcConfigurer {
    
    private RateLimitInterceptor interceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor)
            .addPathPatterns("/api/v1/area/**");
    }
}

The RateLimitInterceptor intercepts each request to our area calculation API endpoints.

RateLimitInterceptor拦截对我们面积计算API端点的每个请求。

Let’s try out our new endpoint:

让我们试试我们的新端点。

## successful request
$ curl -v -X POST http://localhost:9001/api/v1/area/triangle \
    -H "Content-Type: application/json" -H "X-api-key:FX001-99999" \
    -d '{ "height": 15, "base": 8 }'

< HTTP/1.1 200
< X-Rate-Limit-Remaining: 9
{"shape":"triangle","area":60.0}

## rejected request
$ curl -v -X POST http://localhost:9001/api/v1/area/triangle \
    -H "Content-Type: application/json" -H "X-api-key:FX001-99999" \
    -d '{ "height": 15, "base": 8 }'

< HTTP/1.1 429
< X-Rate-Limit-Retry-After-Seconds: 299
{ "status": 429, "error": "Too Many Requests", "message": "You have exhausted your API Request Quota" }

It looks like we’re done. We can keep adding endpoints, and the interceptor will apply the rate limit for each request.

看起来我们已经完成了。我们可以继续添加端点,拦截器会对每个请求应用速率限制。

6. Bucket4j Spring Boot Starter

6.Bucket4j Spring Boot启动器

Let’s look at another way of using Bucket4j in a Spring application. The Bucket4j Spring Boot Starter provides auto-configuration for Bucket4j that helps us achieve API rate limiting via Spring Boot application properties or configuration.

让我们来看看在Spring应用程序中使用Bucket4j的另一种方式。Bucket4j Spring Boot Starter为Bucket4j提供了自动配置,帮助我们通过Spring Boot应用属性或配置实现API速率限制。

Once we integrate the Bucket4j starter into our application, we’ll have a completely declarative API rate limiting implementation, without any application code.

一旦我们把Bucket4j启动器集成到我们的应用程序中,我们将有一个完全声明式的API速率限制实现,而不需要任何应用代码

6.1. Rate Limit Filters

6.1 速率限制过滤器

In our example, we used the value of the request header X-api-key as the key for identifying and applying the rate limits.

在我们的例子中,我们使用请求头的值X-api-key作为识别和应用速率限制的关键。

The Bucket4j Spring Boot Starter provides several predefined configurations for defining our rate limit key:

Bucket4j的Spring Boot启动器提供了几个预定义的配置来定义我们的速率限制键。

  • a naive rate limit filter, which is the default
  • filter by IP Address
  • expression-based filters

Expression-based filters use the Spring Expression Language (SpEL). SpEL provides access to root objects, such as HttpServletRequest, that can be used to build filter expressions on the IP Address (getRemoteAddr()), request headers (getHeader(‘X-api-key’)), and so on.

基于表达式的过滤器使用Spring表达式语言(SpEL)。SpEL提供了对根对象的访问,例如HttpServletRequest,,可以用来建立关于IP地址(getRemoteAddr())、请求头(getHeader(‘X-api-key’))等的过滤器表达式。

The library also supports custom classes in the filter expressions, which is discussed in the documentation.

该库还支持过滤器表达式中的自定义类,这在文档中讨论。

6.2. Maven Configuration

6.2.Maven配置

Let’s begin by adding the bucket4j-spring-boot-starter dependency to our pom.xml:

让我们首先把bucket4j-spring-boot-starter依赖性添加到我们的pom.xml

<dependency>
    <groupId>com.giffing.bucket4j.spring.boot.starter</groupId>
    <artifactId>bucket4j-spring-boot-starter</artifactId>
    <version>0.2.0</version>
</dependency>

We used an in-memory Map to store the Bucket per API key (consumer) in our earlier implementation. Here, we can use Spring’s caching abstraction to configure an in-memory store, such as Caffeine or Guava.

在之前的实现中,我们使用内存Map来存储每个API密钥(消费者)的Bucket。在这里,我们可以使用Spring的缓存抽象来配置一个内存存储,例如CaffeineGuava

Let’s add the caching dependencies:

让我们添加缓存的依赖性。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.8.2</version>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>jcache</artifactId>
    <version>2.8.2</version>
</dependency>

Note: We added the jcache dependencies as well, to conform with Bucket4j’s caching support.

注意:我们也添加了jcache依赖项,以符合Bucket4j的缓存支持。

We must remember to enable the caching feature by adding the @EnableCaching annotation to any of the configuration classes.

我们必须记住通过在任何一个配置类中添加@EnableCaching注解来启用缓存功能

6.3. Application Configuration

6.3.应用配置

Let’s configure our application to use the Bucket4j starter library. First, we’ll configure Caffeine caching to store the API key and Bucket in-memory:

让我们来配置我们的应用程序以使用Bucket4j启动库。首先,我们将配置Caffeine缓存以在内存中存储API密钥和Bucket

spring:
  cache:
    cache-names:
    - rate-limit-buckets
    caffeine:
      spec: maximumSize=100000,expireAfterAccess=3600s

Next, let’s configure Bucket4j:

接下来,让我们配置 Bucket4j。

bucket4j:
  enabled: true
  filters:
  - cache-name: rate-limit-buckets
    url: /api/v1/area.*
    strategy: first
    http-response-body: "{ \"status\": 429, \"error\": \"Too Many Requests\", \"message\": \"You have exhausted your API Request Quota\" }"
    rate-limits:
    - expression: "getHeader('X-api-key')"
      execute-condition: "getHeader('X-api-key').startsWith('PX001-')"
      bandwidths:
      - capacity: 100
        time: 1
        unit: hours
    - expression: "getHeader('X-api-key')"
      execute-condition: "getHeader('X-api-key').startsWith('BX001-')"
      bandwidths:
      - capacity: 40
        time: 1
        unit: hours
    - expression: "getHeader('X-api-key')"
      bandwidths:
      - capacity: 20
        time: 1
        unit: hours

So, what did we just configure?

那么,我们刚刚配置了什么?

  • bucket4j.enabled=true – enables Bucket4j auto-configuration
  • bucket4j.filters.cache-name – gets the Bucket for an API key from the cache
  • bucket4j.filters.url – indicates the path expression for applying the rate limit
  • bucket4j.filters.strategy=first – stops at the first matching rate limit configuration
  • bucket4j.filters.rate-limits.expression – retrieves the key using Spring Expression Language (SpEL)
  • bucket4j.filters.rate-limits.execute-condition – decides whether to execute the rate limit or not using SpEL
  • bucket4j.filters.rate-limits.bandwidths – defines the Bucket4j rate limit parameters

We replaced the PricingPlanService and the RateLimitInterceptor with a list of rate limit configurations that are evaluated sequentially.

我们将PricingPlanServiceRateLimitInterceptor替换为一个依次评估的速率限制配置列表。

Let’s try it out:

让我们来试一试。

## successful request
$ curl -v -X POST http://localhost:9000/api/v1/area/triangle \
    -H "Content-Type: application/json" -H "X-api-key:FX001-99999" \
    -d '{ "height": 20, "base": 7 }'

< HTTP/1.1 200
< X-Rate-Limit-Remaining: 7
{"shape":"triangle","area":70.0}

## rejected request
$ curl -v -X POST http://localhost:9000/api/v1/area/triangle \
    -H "Content-Type: application/json" -H "X-api-key:FX001-99999" \
    -d '{ "height": 7, "base": 20 }'

< HTTP/1.1 429
< X-Rate-Limit-Retry-After-Seconds: 212
{ "status": 429, "error": "Too Many Requests", "message": "You have exhausted your API Request Quota" }

7. Conclusion

7.结论

In this article, we demonstrated several different approaches using Bucket4j for rate-limiting Spring APIs. To learn more, be sure to check out the official documentation.

在这篇文章中,我们演示了使用Bucket4j对Spring API进行速率限制的几种不同方法。要了解更多信息,请务必查看官方的文档

As usual, the source code for all the examples is available over on GitHub.

像往常一样,所有例子的源代码都可以在GitHub上找到