Class RateLimitFilter

java.lang.Object
org.springframework.web.filter.GenericFilterBean
org.springframework.web.filter.OncePerRequestFilter
com.loomcache.springboot.security.RateLimitFilter
All Implemented Interfaces:
jakarta.servlet.Filter, org.springframework.beans.factory.Aware, org.springframework.beans.factory.BeanNameAware, org.springframework.beans.factory.DisposableBean, org.springframework.beans.factory.InitializingBean, org.springframework.context.EnvironmentAware, org.springframework.core.env.EnvironmentCapable, org.springframework.web.context.ServletContextAware

public class RateLimitFilter extends org.springframework.web.filter.OncePerRequestFilter
BLK-2026-04-22-008: in-process token-bucket rate limiter for the REST surface.

Prior posture: per-request payload bounds were enforced by individual controllers, but no global rate limit existed — a single misbehaving client (or an unauthenticated flood targeting /api/cluster/health) could exhaust CPU, queue depth, and persistence/topic fanout even with bounded request bodies.

Design (as hardened by BLK-PHASE4-002 + BLK-PHASE4-003):

  • Filter MUST run inside the Spring Security chain after mTLS/JWT authentication so HttpServletRequest.getUserPrincipal() or SecurityContextHolder is populated. Installing it before authentication would silently treat all authenticated traffic as anonymous and starve real users on the smaller anonymous bucket.
  • One bucket per key: established authenticated principal name when present in the servlet request or Spring Security context, otherwise the source IP. The filter never derives a principal from an unverified token string.
  • Token bucket refills at refillPerSecond tokens/s up to capacity. Anonymous / health probe traffic shares its own (smaller) budget keyed on IP.
  • X-Forwarded-For is honored ONLY when the immediate connection comes from a configured trusted proxy. Without this, an attacker could spoof XFF to forge IP identity and either bypass their own bucket or DoS another tenant's bucket.
  • Bucket map is bounded (MAX_BUCKETS) — overflow rejects only the incoming untracked key. This preserves pressure on existing exhausted buckets while bounding memory under attacker-controlled key churn (rotating IPs / impersonated principals).
  • On reject: HTTP 429 with Retry-After: 1. No attempt to enforce strict fairness — this is a coarse global cap, not a quality-of-service scheduler.

Defaults are intentionally generous so legitimate workloads are unaffected. Operators tune via loomcache.security.rate-limit.* properties.

  • Field Summary

    Fields inherited from class org.springframework.web.filter.OncePerRequestFilter

    ALREADY_FILTERED_SUFFIX

    Fields inherited from class org.springframework.web.filter.GenericFilterBean

    logger
  • Constructor Summary

    Constructors
    Constructor
    Description
    RateLimitFilter(long capacity, long refillPerSecond, long anonymousCapacity, long anonymousRefillPerSecond)
    Convenience constructor with no trusted proxies (XFF ignored).
    RateLimitFilter(long capacity, long refillPerSecond, long anonymousCapacity, long anonymousRefillPerSecond, Set<String> trustedProxyIps)
     
  • Method Summary

    Modifier and Type
    Method
    Description
    protected void
    doFilterInternal(@NonNull jakarta.servlet.http.HttpServletRequest request, @NonNull jakarta.servlet.http.HttpServletResponse response, @NonNull jakarta.servlet.FilterChain filterChain)
     

    Methods inherited from class org.springframework.web.filter.OncePerRequestFilter

    doFilter, doFilterNestedErrorDispatch, getAlreadyFilteredAttributeName, isAsyncDispatch, isAsyncStarted, shouldNotFilter, shouldNotFilterAsyncDispatch, shouldNotFilterErrorDispatch

    Methods inherited from class org.springframework.web.filter.GenericFilterBean

    addRequiredProperty, afterPropertiesSet, createEnvironment, destroy, getEnvironment, getFilterConfig, getFilterName, getServletContext, init, initBeanWrapper, initFilterBean, setBeanName, setEnvironment, setServletContext

    Methods inherited from class Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Constructor Details

    • RateLimitFilter

      public RateLimitFilter(long capacity, long refillPerSecond, long anonymousCapacity, long anonymousRefillPerSecond, Set<String> trustedProxyIps)
      Parameters:
      capacity - burst capacity for authenticated principals
      refillPerSecond - steady-state refill rate for authenticated principals
      anonymousCapacity - burst capacity for anonymous (IP-keyed) traffic
      anonymousRefillPerSecond - steady-state refill rate for anonymous traffic
      trustedProxyIps - IPs whose X-Forwarded-For header is honored; empty = never trust XFF
    • RateLimitFilter

      public RateLimitFilter(long capacity, long refillPerSecond, long anonymousCapacity, long anonymousRefillPerSecond)
      Convenience constructor with no trusted proxies (XFF ignored).
  • Method Details

    • doFilterInternal

      protected void doFilterInternal(@NonNull jakarta.servlet.http.HttpServletRequest request, @NonNull jakarta.servlet.http.HttpServletResponse response, @NonNull jakarta.servlet.FilterChain filterChain) throws jakarta.servlet.ServletException, IOException
      Specified by:
      doFilterInternal in class org.springframework.web.filter.OncePerRequestFilter
      Throws:
      jakarta.servlet.ServletException
      IOException