Rate Limit pro IP für Spring Boot
Um eine Anwendung vor unliebsamen Traffic wie Spam oder andere Formen von automatisierten Requests zu schützen, können Rate Limits genutzt werden. In diesem Artikel beschreibe ich, wie sich Rate Limits pro IP-Adresse in einer Spring Boot Anwendung umsetzen lassen.
Ein Hinweis vorweg: Bei Verwendung eines Reverse Proxies lassen sich Rate Limits auch ohne eigenen Code einrichten. API Gateways und Reverse Proxies wie Traefik oder Nginx bieten in der Regel eine Konfiguration für genau diesen Anwendungsfall an.
Implementierung mit bucket4j
Ist eine Implementierung auf Anwendungsebene gewünscht, bietet sich die Bibliothek bucket4j an. Diese implementiert den Token Bucket Algorithmus und lässt sich einfach in Spring Boot integrieren.
Zunächst muss die Abhängigkeit hinzugefügt werden:
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>...</version>
</dependency>
Rate Limit Filter
Die Kernkomponente unserer Lösung ist ein Custom Filter, der für jede IP-Adresse ein eigenes Bucket verwaltet.
public class RateLimitFilter extends OncePerRequestFilter {
private final PathPatternRequestMatcher matcher =
PathPatternRequestMatcher
.withDefaults()
.matcher("/api/register");
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return !matcher.matches(request);
}
// ...
}
Der PathPatternRequestMatcher
definiert, welche Endpunkte vom Rate Limiting betroffen sind.
In unserem Fall ist es nur der Endpunkt /api/register
.
Für andere Endpunkte wird der Filter nicht konfiguriert.
public class RateLimitFilter extends OncePerRequestFilter {
// ...
private final ConcurrentHashMap<String, Bucket> buckets =
new ConcurrentHashMap<>();
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain fc) {
final String key = req.getRemoteAddr();
var bucket = buckets.computeIfAbsent(key, _ -> createBucket());
if (bucket.tryConsume(1)) {
fc.doFilter(req, res);
} else {
res.sendError(
HttpStatus.TOO_MANY_REQUESTS.value(),
"calm down"
);
}
}
}
Für jede IP-Adresse wird ein eigener Bucket erstellt und in einer ConcurrentHashMap
gespeichert.
Bei jedem Request wird versucht, ein Token aus dem entsprechenden Bucket zu entnehmen.
Ist dies erfolgreich, wird der Request weitergeleitet.
Andernfalls wird der Request mit 429 Too Many Requests
abgelehnt.
Bucket Konfiguration
Die Konfiguration von bucket4j erfolgt per Java Code.
private Bucket createBucket() {
return Bucket.builder().addLimit(
limit -> limit
.capacity(3)
.refillIntervally(3, Duration.ofMinutes(15)))
.build();
}
Folgende Parameter sind hier beispielhaft für das Rate Limiting konfiguriert:
-
3 Tokens pro Bucket
-
Alle 15 Minuten werden 3 neue Tokens hinzugefügt
Diese Konfiguration erlaubt es einer IP-Adresse somit maximal 3 Requests in 15 Minuten zu senden.
Filter einbinden
Der Filter wird in die Spring Security Filter Chain integriert:
@Bean
SecurityFilterChain filterChain(HttpSecurity http) {
http
.addFilterBefore(
new RateLimitFilter(),
UsernamePasswordAuthenticationFilter.class
)
// Zusätzliche Security Konfiguration ...
;
return http.build();
}
Der Filter wird vor dem UsernamePasswordAuthenticationFilter
positioniert, um sicherzustellen, dass das Rate Limiting vor der Authentifizierung stattfindet.
Überlegungen zur Produktion
Die gezeigte Implementierung ist für einfache Anwendungsfälle auch im produktiven Einsatz geeignet. Sie hat einige Einschränkungen, die vor dem produktiven Einsatz definitiv berücksichtigt werden sollten.
Die Map der Buckets wächst mit der Anzahl der verschiedenen IP-Adressen und wird aktuell nicht automatisch bereinigt. Für produktive Anwendungen sollte ein TTL-basierter Cache, beispielsweise Caffeine, verwendet werden.
Die Buckets befinden sich zudem nur im jeweils lokalen Speicher der spezifischen Spring Boot Instanz.
In einer Umgebung, bei der mehrere parallele Instanzen zum Einsatz kommen, sollten die Buckets über Instanzen hinweg synchronisiert werden.
Dies könnte zum Beispiel durch Redis oder Memcached umgesetzt werden.
Eingehende Requests stammen häufig von einem Proxy und nicht direkt vom Client selbst.
Damit in dieser Situation request.getRemoteAddr()
die tatsächliche Client-IP zurückgibt, kann die Property server.forward-headers-strategy=native
gesetzt werden.
Dadurch wird der Header X-Forwarded-For
des Requests ausgewertet und daraus die eigentliche IP ermittelt.
Der Header kann über die Property server.tomcat.remoteip.remote-ip-header
konfiguriert werden.
Für den Umgang mit IPv6-Adressen sollte außerdem berücksichtigt werden, dass Provider häufig relativ große /64
Netzbereiche vergeben.
Somit ist es für einen Client ein leichtes durch sehr viele Adressen zu wechseln, um das Rate Limit zu umgehen.
Eine gängige Praxis ist deshalb, einen Bucket für ein ganzes Subnetz vorzusehen und entsprechend zu dimensionieren.
Natürlich sind auch entsprechende Kombinationen denkbar.
Zu den Themen Spring Boot und Spring Security bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:
Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.