Class LoginRateLimiter
Maintains a sliding window of attempt timestamps per (scope, key) pair plus
an optional lockout-until timestamp. Three scopes are recognized: IP, USER
and CHAT. LoginRateLimiter.Scope.CHAT is used by the v2 chat endpoints to throttle
per-user chat invocations independently of the login buckets.
Thresholds are passed in per call so the manager can read FessConfig once and
forward the values; this keeps the helper test-friendly (no static config).
Eviction: each allow(org.codelibs.fess.api.v2.handlers.LoginRateLimiter.Scope, java.lang.String, int, int) call drops timestamps older than the window
from the head of the queue. Idle keys remain in the map until the next call;
sweep() can be invoked periodically to evict empty entries.
Memory cap: the entries map is bounded by effectiveCap. When the cap
is reached, eviction picks the oldest idle entry (no in-window hits AND
no active lockout) — M-4: locked-out entries are never evicted, otherwise an
attacker could pump bogus usernames to silently release a victim's lockout. If
every entry is either locked or active the map is allowed to grow above cap by
one and a WARN is logged (best-effort policy: prefer correctness over a strict
cap). The cap defaults to DEFAULT_MAX_ENTRIES (100,000) and can be
tuned via the theme.api.login.rate.limit.max.entries property.
Sweep schedule: on DI init a TimeoutManager periodic task calls
sweep() every SWEEP_INTERVAL_SECONDS seconds (default 300).
This is the same background-task pattern used by CoordinatorHelper and
LogNotificationHelper in this codebase.
MJ-30 i18n note: error.message is developer-facing English. Clients MUST use error.code (the V2ErrorCode token) for user-facing i18n.
Multi-node deployments require LB-level rate limiting; see Plan 3 risks.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic enumRate-limit scope: distinguishes the bucket namespace so the samekeyvalue (e.g. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionbooleanallow(LoginRateLimiter.Scope scope, String key, int maxPerWindow, int windowSeconds) Attempts to consume one slot in the (scope, key) bucket within a sliding window ofwindowSeconds.voidclear(LoginRateLimiter.Scope scope, String key) Clears the rate-limit bucket for (scope, key) after a successful login so that the user is not penalized for earlier failed attempts in the same window.voiddestroy()Stops the background sweep task on shutdown.voidinit()Called by Lasta Di after the component is bound.voidlockOut(LoginRateLimiter.Scope scope, String key, int lockoutSeconds) m-21: uses Math.max so that a shorter lockoutSeconds value supplied in a second call never shrinks an existing longer lockout — the stricter deadline always wins.booleanpeek(LoginRateLimiter.Scope scope, String key, int maxPerWindow, int windowSeconds) Returns true when a fresh attempt against (scope, key) would currently be admitted byallow(org.codelibs.fess.api.v2.handlers.LoginRateLimiter.Scope, java.lang.String, int, int), but without consuming a bucket slot.voidsweep()Removes empty entries; safe to call from any thread.
-
Constructor Details
-
LoginRateLimiter
public LoginRateLimiter()Default constructor used by the DI container. UsesSystem.currentTimeMillis()as the clock andDEFAULT_MAX_ENTRIESas the initial cap;init()may override the cap fromFessConfigonce DI is fully wired.
-
-
Method Details
-
init
@PostConstruct public void init()Called by Lasta Di after the component is bound. Reads thetheme.api.login.rate.limit.max.entriesproperty (if present) to override the default cap, then registers a periodic sweep task viaTimeoutManager— the same pattern used by CoordinatorHelper and LogNotificationHelper. Sweep runs everySWEEP_INTERVAL_SECONDSseconds to evict entries whose windows have expired. -
destroy
@PreDestroy public void destroy()Stops the background sweep task on shutdown. -
allow
Attempts to consume one slot in the (scope, key) bucket within a sliding window ofwindowSeconds. Expired timestamps at the head of the queue are evicted first; if the bucket is currently locked out or already atmaxPerWindowthe call returnsfalsewithout recording a hit.MJ-6: empty or null
keyis denied rather than silently allowed; an empty key usually means the caller failed to resolve the client identity, and granting access would bypass the gate entirely.- Parameters:
scope- rate-limit scope (e.g. IP, USER, CHAT)key- bucket key (e.g. IP address or username)maxPerWindow- upper bound of attempts permitted withinwindowSeconds; values<= 0disable the gate and returntruewindowSeconds- sliding-window width in seconds- Returns:
trueif the slot was consumed,falseif the bucket is full or locked out
-
peek
Returns true when a fresh attempt against (scope, key) would currently be admitted byallow(org.codelibs.fess.api.v2.handlers.LoginRateLimiter.Scope, java.lang.String, int, int), but without consuming a bucket slot. Use this to short-circuit work (e.g. credential verification) when the bucket is already exhausted, then callallow(org.codelibs.fess.api.v2.handlers.LoginRateLimiter.Scope, java.lang.String, int, int)only on the failure path so success and system-error paths do not count against the legitimate user's quota.MJ-6: returns
false(deny) when key is null or empty — defense in depth. Empty key means the caller could not resolve the identity; passing rather than denying would silently bypass the gate.- Parameters:
scope- rate-limit scope (e.g. IP, USER, CHAT)key- bucket key (e.g. IP address or username)maxPerWindow- upper bound of attempts permitted withinwindowSecondswindowSeconds- sliding-window width- Returns:
- true if a subsequent
allow(org.codelibs.fess.api.v2.handlers.LoginRateLimiter.Scope, java.lang.String, int, int)would currently succeed
-
lockOut
m-21: uses Math.max so that a shorter lockoutSeconds value supplied in a second call never shrinks an existing longer lockout — the stricter deadline always wins.- Parameters:
scope- rate-limit scope (e.g. IP, USER, CHAT) that the lockout applies tokey- bucket key being locked out (e.g. IP address or username); ignored whennullor emptylockoutSeconds- duration of the lockout in seconds; ignored when<= 0
-
clear
Clears the rate-limit bucket for (scope, key) after a successful login so that the user is not penalized for earlier failed attempts in the same window.MJ-5: called by LoginHandler on the successful-login path for both Scope.USER (by username) and Scope.IP (by client IP). This prevents a legitimate user who previously exceeded the window from being locked out on their next login after the correct password is supplied.
- Parameters:
scope- the rate-limit scopekey- the bucket key (username or IP address)
-
sweep
public void sweep()Removes empty entries; safe to call from any thread.
-