Neuigkeiten von trion.
Immer gut informiert.

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:

Abhängigkeit in Maven pom.xml
<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.

RateLimitFilter Teil 1
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.

RateLimitFilter Teil 2
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.

RateLimitFilter Teil 3 - Konfiguration
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:

Security Konfiguration
    @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.



Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Zur Desktop Version des Artikels