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()orSecurityContextHolderis 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
refillPerSecondtokens/s up tocapacity. Anonymous / health probe traffic shares its own (smaller) budget keyed on IP. X-Forwarded-Foris 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_SUFFIXFields inherited from class org.springframework.web.filter.GenericFilterBean
logger -
Constructor Summary
ConstructorsConstructorDescriptionRateLimitFilter(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 TypeMethodDescriptionprotected voiddoFilterInternal(@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, shouldNotFilterErrorDispatchMethods inherited from class org.springframework.web.filter.GenericFilterBean
addRequiredProperty, afterPropertiesSet, createEnvironment, destroy, getEnvironment, getFilterConfig, getFilterName, getServletContext, init, initBeanWrapper, initFilterBean, setBeanName, setEnvironment, setServletContext
-
Constructor Details
-
RateLimitFilter
public RateLimitFilter(long capacity, long refillPerSecond, long anonymousCapacity, long anonymousRefillPerSecond, Set<String> trustedProxyIps) - Parameters:
capacity- burst capacity for authenticated principalsrefillPerSecond- steady-state refill rate for authenticated principalsanonymousCapacity- burst capacity for anonymous (IP-keyed) trafficanonymousRefillPerSecond- steady-state refill rate for anonymous traffictrustedProxyIps- 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:
doFilterInternalin classorg.springframework.web.filter.OncePerRequestFilter- Throws:
jakarta.servlet.ServletExceptionIOException
-