Spring Boot Starter for Bucket4j
Project version overview:
-
0.9.0 - Bucket4j 8.1.0 - Spring Boot 3.0.x
-
0.8.0 - Bucket4j 8.1.0 - Spring Boot 2.7.x
-
0.7.0 - Bucket4j 7.5.0 - Spring Boot 2.7.x
Examples:
Contents
Introduction
This project is a Spring Boot Starter for Bucket4j. It can be used limit the rate of access to your REST APIs.
-
Prevention of DoS Attacks, brute-force logins attempts
-
Request throttling for specific regions, unauthenticated users, authenticated users, not paying users.
The benefit of this project is the configuration of Bucket4j via Spring Boots properties or yaml files. You donβt have to write a single line of code.
Migration Guide
This section is meant to help you migrate your application to new version of this starter project.
Spring Boot Starter Bucket4j 0.9
-
Upgrade to Spring Boot 3
-
Spring Boot 3 requires Java 17 so use at least Java 17
-
Replaced Java 8 compatible Bucket4j dependencies
-
Exclude example webflux-infinispan due to startup problems
Spring Boot Starter Bucket4j 0.8
Compatibility to Java 8
The version 0.8 tries to be compatible with Java 8 as long as Bucket4j is supporting Java 8. With the release of Bucket4j 8.0.0 Bucket4j decided to migrate to Java 11 but provides dedicated artifacts for Java 8. The project is switching to the dedicated artifacts which supports Java 8. You can read more about it here.
Rename property expression to cache-key
The property ..rate-limits[0].expression is renamed to ..rate-limits[0].cache-key. An Exception is thrown on startup if the expression property is configured.
To ensure that the property is not filled falsely the property is marked with @Null. This change requires a Bean Validation implementation.
JSR 380 - Bean Validation implementation required
To ensure that the Bucket4j property configuration is correct an Validation API implementation is required. You can add the Spring Boot Starter Validation which will automatically configures one.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Explicit Configuration of the Refill Speed - API Break
The refill speed of the Buckets can now configured explicitly with the Enum RefillSpeed. You can choose between a greedy or interval refill see the official documentation.
Before 0.8 the refill speed was configured implicitly by setting the fixed-refill-interval property explicit.
bucket4j.filters[0].rate-limits[0].bandwidths[0].fixed-refill-interval=0
bucket4j.filters[0].rate-limits[0].bandwidths[0].fixed-refill-interval-unit=minutes
These properties are removed and replaced by the following configuration:
bucket4j.filters[0].rate-limits[0].bandwidths[0].refill-speed=interval
You can read more about the refill speed configuration here Refill Speed
Getting started
To use the rate limit in your project you have to add the Bucket4j Spring Boot Starter dependency in your project. Additionally you have to choose a caching provider [cache_overview].
The next example uses JSR 107 Ehcache which will be auto configured with the Spring Boot Starter Cache.
<dependency>
<groupId>com.giffing.bucket4j.spring.boot.starter</groupId>
<artifactId>bucket4j-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
Donβt forget to enable the caching feature by adding the @EnableCaching annotation to any of the configuration classes.
The configuration can be done in the application.properties / application.yml. The following configuration limits all requests independently from the user. It allows a maximum of 5 requests within 10 seconds independently from the user.
bucket4j:
enabled: true
filters:
- cache-name: buckets
url: .*
rate-limits:
- bandwidths:
- capacity: 5
time: 10
unit: seconds
For Ehcache 3 you also need a ehcache.xml which can be placed in the classpath. The configured cache name buckets must be defined in the configuration file.
spring:
cache:
jcache:
config: classpath:ehcache.xml
<config xmlns="...">
<cache alias="buckets">
<expiry>
<ttl unit="seconds">3600</ttl>
</expiry>
<heap unit="entries">1000000</heap>
</cache>
</config>
Overview Cache Autoconfiguration
The following list contains the Caching implementation which will be autoconfigured by this starter.
Reactive |
Name |
cache-to-use |
N |
jcache |
|
Yes |
jcache-ignite |
|
no |
hazelcast-spring |
|
yes |
hazelcast-reactive |
|
Yes |
infinispan |
|
No |
redis-jedis |
|
Yes |
redis-lettuce |
|
Yes |
redis-redisson |
|
No |
redis-springdata |
Instead of determine the Caching Provider by the Bucket4j Spring Boot Starter project you can implement the SynchCacheResolver or the AsynchCacheResolver by yourself.
You can enable the cache auto configuration explicitly by using the cache-to-use property name or setting it to an invalid value to disable all auto configurations.
bucket4j.cache-to-use=jcache #
Filter
Filter strategy
The filter strategy defines how the execution of the rate limits will be performed.
bucket4j.filters[0].strategy=first # [first, all]
first
The first is the default strategy. This the default strategy which only executes one rate limit configuration.
all
The all strategy executes all rate limit independently.
Cache Key
To differentiate incoming request you can provide an expression which is used as a key resolver for the underlying cache.
The expression uses the Spring Expression Language (SpEL) which provides the most flexible solution to determine the cache key written in one line of code. The expression compiles to a Java class which will be used.
Depending on the filter method [servlet,webflux,gateway] different SpEL root objects object can be used in the expression so that you have a direct access to the method of these request objects:
-
servlet: jakarta.servlet.http.HttpServletRequest (e.g. getRemoteAddr() or getRequestURI())
-
webflux: org.springframework.http.server.reactive.ServerHttpRequest
-
gateway: org.springframework.http.server.reactive.ServerHttpRequest
The configured URL which is used for filtering is added to the cache-key to provide a unique cache-key for multiple URL. You can read more about it here.
Limiting based on IP-Address:
getRemoteAddress()
Limiting based on Username - If not logged in use IP-Address:
@securityService.username()?: getRemoteAddr()
/**
* You can define custom beans like the SecurityService which can be used in the SpEl expressions.
**/
@Service
public class SecurityService {
public String username() {
String name = SecurityContextHolder.getContext().getAuthentication().getName();
if(name == "anonymousUser") {
return null;
}
return name;
}
}
Refill Speed
The refill speed defines the period of the regeneration of consumed tokens. This starter supports two types of token regeneration. The refill speed can be set with the following property:
bucket4j.filters[0].rate-limits[0].bandwidths[0].refill-speed=greedy # [greedy,interval]
-
greedy: This is the default refill speed and tries to add tokens as soon as possible.
-
interval: You can alternatively chose interval for the token regeneration which refills the token in a fixed interval.
You can read more about the refill speed in the official documentation.
Skip and Execution Predicates
Skip and Execution Predicates can be used to conditionally skip or execute the rate limiting. Each predicate has a unique name and a self-contained configuration. The following section describes the build in Execution Predicates and how to use them.
Path Predicates
The Path Predicate takes a list of path parameters where any of the paths must match. See PathPattern for the available configuration options. Segments are not evaluated further.
bucket4j.filters[0].rate-limits[0].skip-predicates[0]=PATH=/hello,/world,/admin
bucket4j.filters[0].rate-limits[0].execute-predicates[0]=PATH=/hello,/world,/admin
Matches the paths '/hello', '/world' or '/admin'.
Method Predicate
The Method Predicate takes a list of method parameters where any of the methods must match the used HTTP method.
bucket4j.filters[0].rate-limits[0].skip-predicates[0]=METHOD=GET,POST bucket4j.filters[0].rate-limits[0].execute-predicates[0]=METHOD=GET,POST
Matches if the HTTP method is 'GET' or 'POST'.
Query Predicate
The Query Predicate takes a single parameter to check for the existence of the query parameter.
bucket4j.filters[0].rate-limits[0].skip-predicates[0]=QUERY=PARAM_1 bucket4j.filters[0].rate-limits[0].execute-predicates[0]=QUERY=PARAM_1
Matches if the query parameter 'PARAM_1' exists.
Header Predicate
The Header Predicate takes to parameters.
-
First - The name of the Header Parameter which must match exactly
-
Second - An optional regular expression where any existing header under the name must match
bucket4j.filters[0].rate-limits[0].execute-predicates[0]=Content-Type,.*PDF.*
Matches if the query parameter 'PARAM_1' exists.
Custom Predicate
You can also define you own Execution Predicate:
@Component
@Slf4j
public class MyQueryExecutePredicate extends ExecutePredicate<HttpServletRequest> {
private String query;
public String name() {
// The name which can be used on the properties
return "MY_QUERY";
}
public boolean test(HttpServletRequest t) {
// the logic to implement the predicate
boolean result = t.getParameterMap().containsKey(query);
log.debug("my-query-parametetr;value:%s;result:%s".formatted(query, result));
return result;
}
public ExecutePredicate<HttpServletRequest> parseSimpleConfig(String simpleConfig) {
// the configuration which is configured behind the equal sign
// MY_QUERY=P_1 -> simpleConfig == "P_1"
//
this.query = simpleConfig;
return this;
}
}
Bucket4j properties
bucket4j.enabled=true # enable/disable bucket4j support
bucket4j.cache-to-use= # If you use multiple caching implementation in your project and you want to choose a specific one you can set the cache here (jcache, hazelcast, ignite, redis)
bucket4j.filters[0].cache-name=buckets # the name of the cache key
bucket4j.filters[0].filter-method=servlet # [servlet,webflux,gateway]
bucket4j.filters[0].filter-order= # Per default the lowest integer plus 10. Set it to a number higher then zero to execute it after e.g. Spring Security.
bucket4j.filters[0].http-content-type=application/json
bucket4j.filters[0].http-status-code=TOO_MANY_REQUESTS # Enum value of org.springframework.http.HttpStatus
bucket4j.filters[0].http-response-body={ "message": "Too many requests" } # the json response which should be added to the body
bucket4j.filters[0].http-response-headers.<MY_CUSTOM_HEADER>=MY_CUSTOM_HEADER_VALUE # You can add any numbers of custom headers
bucket4j.filters[0].hide-http-response-headers=true # Hides response headers like x-rate-limit-remaining or x-rate-limit-retry-after-seconds on rate limiting
bucket4j.filters[0].url=.* # a regular expression
bucket4j.filters[0].strategy=first # [first, all] if multiple rate limits configured the 'first' strategy stops the processing after the first matching
bucket4j.filters[0].rate-limits[0].cache-key=getRemoteAddr() # defines the cache key. It will be evaluated with the Spring Expression Language
bucket4j.filters[0].rate-limits[0].num-tokens=1 # The number of tokens to consume
bucket4j.filters[0].rate-limits[0].execute-condition=1==1 # an optional SpEl expression to decide to execute the rate limit or not
bucket4j.filters[0].rate-limits[0].execute-predicates[0]=PATH=/hello,/world # On the HTTP Path as a list
bucket4j.filters[0].rate-limits[0].execute-predicates[1]=METHOD=GET,POST # On the HTTP Method
bucket4j.filters[0].rate-limits[0].execute-predicates[2]=QUERY=HELLO # Checks for the existence of a Query Parameter
bucket4j.filters[0].rate-limits[0].skip-condition=1==1 # an optional SpEl expression to skip the rate limit
bucket4j.filters[0].rate-limits[0].bandwidths[0].capacity=10
bucket4j.filters[0].rate-limits[0].bandwidths[0].refill-capacity= # default is capacity
bucket4j.filters[0].rate-limits[0].bandwidths[0].time=1
bucket4j.filters[0].rate-limits[0].bandwidths[0].unit=minutes
bucket4j.filters[0].rate-limits[0].bandwidths[0].initial-capacity= # Optional initial tokens
bucket4j.filters[0].rate-limits[0].bandwidths[0].refill-speed=greedy # [greedy,interval]
bucket4j.filters[0].metrics.enabled=true
bucket4j.filters[0].metrics.types=CONSUMED_COUNTER,REJECTED_COUNTER # (optional) if your not interested in the consumed counter you can specify only the rejected counter
bucket4j.filters[0].metrics.tags[0].key=IP
bucket4j.filters[0].metrics.tags[0].expression=getRemoteAddr()
bucket4j.filters[0].metrics.tags[0].types=REJECTED_COUNTER # (optionial) this tag should for example only be applied for the rejected counter
bucket4j.filters[0].metrics.tags[1].key=URL
bucket4j.filters[0].metrics.tags[1].expression=getRequestURI()
bucket4j.filters[0].metrics.tags[2].key=USERNAME
bucket4j.filters[0].metrics.tags[2].expression[email protected]() != null ? @securityService.username() : 'anonym'
# Optional default metric tags for all filters
bucket4j.default-metric-tags[0].key=IP
bucket4j.default-metric-tags[0].expression=getRemoteAddr()
bucket4j.default-metric-tags[0].types=REJECTED_COUNTER
# Hide HTTP response headers
Monitoring - Spring Boot Actuator
Spring Boot ships with a great support for collecting metrics. This project automatically provides metric information about the consumed and rejected buckets. You can extend these information with configurable custom tags like the username or the IP-Address which can then be evaluated in a monitoring system like prometheus/grafana.
bucket4j:
enabled: true
filters:
- cache-name: buckets
filter-method: servlet
filter-order: 1
url: .*
metrics:
tags:
- key: IP
expression: getRemoteAddr()
types: REJECTED_COUNTER # for data privacy reasons the IP should only be collected on bucket rejections
- key: USERNAME
expression: "@securityService.username() != null ? @securityService.username() : 'anonym'"
- key: URL
expression: getRequestURI()
rate-limits:
- execute-condition: "@securityService.username() == 'admin'"
expression: "@securityService.username()?: getRemoteAddr()"
bandwidths:
- capacity: 30
time: 1
unit: minutes
Configuration via properties
Simple configuration to allow a maximum of 5 requests within 10 seconds independently from the user.
bucket4j:
enabled: true
filters:
- cache-name: buckets
url: .*
rate-limits:
- bandwidths:
- capacity: 5
time: 10
unit: seconds
Conditional filtering depending of anonymous or logged in user. Because the bucket4j.filters[0].strategy is first you havnβt to check in the second rate-limit that the user is logged in. Only the first one is executed.
bucket4j:
enabled: true
filters:
- cache-name: buckets
filter-method: servlet
url: .*
rate-limits:
- execute-condition: @securityService.notSignedIn() # only for not logged in users
expression: "getRemoteAddr()"
bandwidths:
- capacity: 10
time: 1
unit: minutes
- execute-condition: "@securityService.username() != 'admin'" # strategy is only evaluate first. so the user must be logged in and user is not admin
expression: @securityService.username()
bandwidths:
- capacity: 1000
time: 1
unit: minutes
- execute-condition: "@securityService.username() == 'admin'" # user is admin
expression: @securityService.username()
bandwidths:
- capacity: 1000000000
time: 1
unit: minutes
Configuration of multiple independently filters (servlet|gateway|webflux filters) with specific rate limit configurations.
bucket4j:
enabled: true
filters: # each config entry creates one servlet filter or other filter
- cache-name: buckets # create new servlet filter with bucket4j configuration
url: /admin*
rate-limits:
bandwidths: # maximum of 5 requests within 10 seconds
- capacity: 5
time: 10
unit: seconds
- cache-name: buckets
url: /public*
rate-limits:
- expression: getRemoteAddress() # IP based filter
bandwidths: # maximum of 5 requests within 10 seconds
- capacity: 5
time: 10
unit: seconds
- cache-name: buckets
url: /users*
rate-limits:
- skip-condition: "@securityService.username() == 'admin'" # we don't check the rate limit if user is the admin user
expression: "@securityService.username()?: getRemoteAddr()" # use the username as key. if authenticated use the ip address
bandwidths:
- capacity: 100
time: 1
unit: seconds
- capacity: 10000
time: 1
unit: minutes