You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何防止无限重试——基于Apache Shiro的RESTful服务

Fixing Brute-force Login Risks in RESTEasy + Apache Shiro

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 CredentialsMatcher to 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 Application subclass 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 08:10:19