如何防止无限重试——基于Apache Shiro的RESTful服务
Great question—you're right to be concerned about bulk account lockouts from brute-force attacks, even with per-user failure limits in place. Let's break down practical, stack-specific fixes to harden your setup:
1. Add IP-Based Rate Limiting (Complement Per-User Locks)
Your current per-user lock stops individual accounts from being brute-forced, but attackers can cycle through random usernames to lock all valid users. Adding IP-level limits blocks this vector:
- Extend Shiro's
CredentialsMatcherto track failed attempts by client IP alongside user accounts. Use a cache (like Redis) to store counts with expiration to avoid permanent IP bans. - Example code snippet:
public class LockingCredentialsMatcher extends HashedCredentialsMatcher { private CacheManager cacheManager; private static final String IP_FAILED_PREFIX = "ip_fail_"; private static final int IP_FAILURE_LIMIT = 10; // 10 fails in 15 mins private static final int IP_LOCK_MINS = 15; @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { boolean isMatch = super.doCredentialsMatch(token, info); if (!isMatch) { // Update your existing user failure count logic here // Add IP tracking String clientIp = getClientIp(); Cache<String, Integer> ipCache = cacheManager.getCache("ipFailedAttempts"); int failedCount = ipCache.get(IP_FAILED_PREFIX + clientIp) == null ? 1 : ipCache.get(IP_FAILED_PREFIX + clientIp) + 1; ipCache.put(IP_FAILED_PREFIX + clientIp, failedCount); // Lock IP if threshold hit if (failedCount >= IP_FAILURE_LIMIT) { Cache<String, Boolean> ipBlacklist = cacheManager.getCache("ipBlacklist"); ipBlacklist.put(clientIp, true); // Set expiration for temporary lock ((RedisCache) ipBlacklist).expire(clientIp, IP_LOCK_MINS, TimeUnit.MINUTES); } } return isMatch; } private String getClientIp() { // Fetch IP from RESTEasy's request context HttpServletRequest request = ResteasyProviderFactory.getContextData(HttpServletRequest.class); return request.getRemoteAddr(); } }
- Attach this custom matcher to your Shiro Realm in your security config.
2. RESTEasy-Level Login Rate Limiting
Add a pre-authentication filter at the RESTEasy layer to block excessive login requests before they reach Shiro:
@Provider @Priority(Priorities.AUTHENTICATION) public class LoginRateLimitFilter implements ContainerRequestFilter { private CacheManager cacheManager; private static final String LOGIN_REQ_PREFIX = "login_req_"; private static final int REQS_PER_MINUTE = 5; @Override public void filter(ContainerRequestContext ctx) throws IOException { String path = ctx.getUriInfo().getPath(); if ("/api/auth/login".equals(path)) { String clientIp = getClientIp(ctx); Cache<String, Integer> reqCache = cacheManager.getCache("loginRequestCounts"); int reqCount = reqCache.get(LOGIN_REQ_PREFIX + clientIp) == null ? 1 : reqCache.get(LOGIN_REQ_PREFIX + clientIp) + 1; reqCache.put(LOGIN_REQ_PREFIX + clientIp, reqCount); if (reqCount > REQS_PER_MINUTE) { ctx.abortWith(Response.status(Response.Status.TOO_MANY_REQUESTS) .entity("Too many login attempts—please wait 1 minute.") .build()); } } } private String getClientIp(ContainerRequestContext ctx) { String forwardedIp = ctx.getHeaderString("X-Forwarded-For"); return forwardedIp != null ? forwardedIp.split(",")[0].trim() : ctx.getUriInfo().getRequestUri().getHost(); } }
- Register this filter in your RESTEasy
Applicationsubclass to activate it.
3. Replace Permanent User Locks with Temporary Ones
Instead of locking users indefinitely after 3 failures, use temporary locks to minimize impact:
- When a user hits 3 failures, store their locked status in a cache with an expiration (e.g., 60 minutes):
// In your Realm's authentication logic if (failedAttempts >= 3) { Cache<String, Boolean> userLocks = cacheManager.getCache("userLocks"); userLocks.put(username, true); ((RedisCache) userLocks).expire(username, 60, TimeUnit.MINUTES); throw new LockedAccountException("Account temporarily locked—try again in 1 hour."); }
4. Add CAPTCHA After 2 Failed Attempts
For an extra layer, force CAPTCHA verification once a user has 2 failed login attempts:
- Integrate a CAPTCHA library (like Kaptcha) to generate and validate codes.
- Modify your login flow to return a "CAPTCHA required" response after 2 failures.
- In your Shiro authentication logic, validate the CAPTCHA before checking credentials when failure count is >=2.
内容的提问来源于stack exchange,提问作者Rico

