5 Proven Strategies to Prevent API Abuse in Spring Boot Applications

This article explores five practical techniques—annotation‑based limits, token‑bucket algorithm, Redis‑Lua distributed limiting, Sentinel integration, and captcha/behavior analysis—to protect Spring Boot APIs from malicious high‑frequency requests while balancing performance and user experience.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
5 Proven Strategies to Prevent API Abuse in Spring Boot Applications

Why API Rate Limiting Matters

In modern internet environments, preventing abusive high‑frequency requests is essential for system stability and security; such attacks can exhaust resources, cause data anomalies, or even crash services.

1. Annotation‑Based Access Frequency Limiting

Implementation Steps

1.1 Create a Rate‑Limit Annotation

Define a custom annotation with attributes time (window), count (max requests), key (rate‑limit key), and message (error message).

1.2 Implement the Rate‑Limit Aspect

@Aspect
@Component
@Slf4j
public class RateLimitAspect {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
        String methodName = pjp.getSignature().getName();
        String className = pjp.getTarget().getClass().getName();
        String limitKey = getLimitKey(pjp, rateLimit, methodName, className);
        int time = rateLimit.time();
        int count = rateLimit.count();
        boolean limited = isLimited(limitKey, time, count);
        if (limited) {
            throw new RuntimeException(rateLimit.message());
        }
        return pjp.proceed();
    }
    // getLimitKey, isLimited, getIpAddress methods omitted for brevity
}

1.3 Usage Example

@RateLimit(time = 60, count = 3, message = "Too many requests")
public User getUser(Long id) { ... }

Pros and Cons

Simple to implement : Minimal code, easy to adopt.

Non‑intrusive : Uses annotations, leaving business logic untouched.

Fine‑grained control : Different limits per method.

Limited flexibility : Fixed window cannot handle burst traffic well.

No pre‑warning : Users are not notified before being blocked.

2. Token Bucket Algorithm

Implementation Steps

2.1 Add Guava Dependency

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

2.2 Create Token Bucket Rate Limiter

@Component
public class RateLimiter {
    private final ConcurrentHashMap<String, com.google.common.util.concurrent.RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();
    public com.google.common.util.concurrent.RateLimiter getRateLimiter(String key, double permitsPerSecond) {
        return rateLimiterMap.computeIfAbsent(key, k -> com.google.common.util.concurrent.RateLimiter.create(permitsPerSecond));
    }
    public boolean tryAcquire(String key, double permitsPerSecond, long timeout, TimeUnit unit) {
        return getRateLimiter(key, permitsPerSecond).tryAcquire(1, timeout, unit);
    }
}

2.3 Create Interceptor

@Component
public class TokenBucketInterceptor implements HandlerInterceptor {
    @Autowired
    private RateLimiter rateLimiter;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!request.getRequestURI().startsWith("/api/")) return true;
        String ip = getIpAddress(request);
        String key = ip + ":" + request.getRequestURI();
        if (!rateLimiter.tryAcquire(key, 10, 1, TimeUnit.SECONDS)) {
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            response.getWriter().write("{\"code\":429,\"message\":\"Too many requests\"}");
            return false;
        }
        return true;
    }
    // getIpAddress omitted for brevity
}

Pros and Cons

Handles bursts : Allows short spikes while smoothing overall traffic.

Simple in‑memory implementation : No external storage needed.

Not suitable for distributed deployments : State is local to a single instance.

State loss on restart : Token bucket resets when the application restarts.

3. Distributed Rate Limiting (Redis + Lua)

Implementation Steps

3.1 Define Lua Script

3.2 Create Redis Rate Limiter Service

@Service
@Slf4j
public class RedisRateLimiterService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    private DefaultRedisScript<Long> rateLimiterScript;
    @PostConstruct
    public void init() {
        rateLimiterScript = new DefaultRedisScript<>();
        rateLimiterScript.setLocation(new ClassPathResource("scripts/rate_limiter.lua"));
        rateLimiterScript.setResultType(Long.class);
    }
    public long isAllowed(String key, int window, int threshold) {
        try {
            List<String> keys = Collections.singletonList(key);
            Long remaining = redisTemplate.execute(rateLimiterScript, keys, String.valueOf(window), String.valueOf(threshold), String.valueOf(System.currentTimeMillis()));
            return remaining == null ? -1 : remaining;
        } catch (Exception e) {
            log.error("Redis rate limiter error", e);
            return threshold;
        }
    }
}

3.3 Create Distributed Rate‑Limit Annotation

3.4 Implement Distributed Aspect

@Aspect
@Component
@Slf4j
public class DistributedRateLimitAspect {
    @Autowired
    private RedisRateLimiterService rateLimiterService;
    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint pjp, DistributedRateLimit rateLimit) throws Throwable {
        String key = generateKey(pjp, rateLimit);
        long remaining = rateLimiterService.isAllowed(key, rateLimit.window(), rateLimit.threshold());
        if (remaining < 0) {
            throw new RuntimeException("Too many requests");
        }
        return pjp.proceed();
    }
    // generateKey, getIpAddress, getUserId omitted for brevity
}

Pros and Cons

Distributed support : Multiple instances share rate‑limit state via Redis.

Precise sliding‑window counting : Lua script ensures atomicity.

Redis dependency : Requires a reliable Redis cluster.

Higher complexity : Lua scripting and Redis configuration increase implementation effort.

4. Sentinel Integration

Sentinel provides comprehensive flow control, circuit breaking, and system protection for Spring Cloud applications.

Implementation Steps

4.1 Add Sentinel Dependency

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2021.0.4.0</version>
</dependency>

4.2 Configure Sentinel

# Sentinel dashboard address
spring.cloud.sentinel.transport.dashboard=localhost:8080
# Eager init
spring.cloud.sentinel.eager=true
spring.application.name=my-application

4.3 Define Flow Rules

4.4 Global Exception Handler

@RestControllerAdvice
public class SentinelExceptionHandler {
    @ExceptionHandler(BlockException.class)
    public Result handle(BlockException e) {
        String message = "Too many requests";
        if (e instanceof FlowException) {
            message = "Flow limit: " + message;
        } else if (e instanceof DegradeException) {
            message = "Service degraded: " + message;
        }
        return Result.error(429, message);
    }
}

Pros and Cons

Feature‑rich : Supports QPS, thread, hotspot parameter limiting, circuit breaking, and system protection.

Dynamic rule management : Rules can be updated via console without redeploy.

Steep learning curve : Rich feature set adds configuration complexity.

Additional dependency : Increases project size and operational overhead.

5. Captcha and Behavior Analysis

For sensitive operations (login, registration, payment), combine image captchas with behavioral analysis to distinguish humans from bots.

Implementation Steps

5.1 Add Captcha Dependency

<dependency>
    <groupId>com.github.whvcse</groupId>
    <artifactId>easy-captcha</artifactId>
    <version>1.6.2</version>
</dependency>

5.2 Captcha Service

@Service
public class CaptchaService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    private static final long CAPTCHA_EXPIRE_TIME = 5 * 60; // 5 minutes
    public String generateCaptcha(HttpServletRequest request, HttpServletResponse response) {
        SpecCaptcha captcha = new SpecCaptcha(130, 48, 5);
        String captchaId = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set("captcha:" + captchaId, captcha.text().toLowerCase(), CAPTCHA_EXPIRE_TIME, TimeUnit.SECONDS);
        Cookie cookie = new Cookie("captchaId", captchaId);
        cookie.setMaxAge((int) CAPTCHA_EXPIRE_TIME);
        cookie.setPath("/");
        response.addCookie(cookie);
        return captcha.toBase64();
    }
    public boolean validateCaptcha(HttpServletRequest request, String captchaCode) {
        String captchaId = null;
        for (Cookie c : request.getCookies()) {
            if ("captchaId".equals(c.getName())) { captchaId = c.getValue(); break; }
        }
        if (captchaId == null) return false;
        String key = "captcha:" + captchaId;
        String correct = redisTemplate.opsForValue().get(key);
        if (correct != null && correct.equals(captchaCode.toLowerCase())) {
            redisTemplate.delete(key);
            return true;
        }
        return false;
    }
}

5.3 Captcha Controller

@RestController
@RequestMapping("/api/captcha")
public class CaptchaController {
    @Autowired
    private CaptchaService captchaService;
    @GetMapping
    public Map<String, String> getCaptcha(HttpServletRequest request, HttpServletResponse response) {
        String base64 = captchaService.generateCaptcha(request, response);
        return Map.of("captcha", base64);
    }
}

5.4 Captcha Annotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CaptchaRequired {
    String captchaParam() default "captchaCode";
}

5.5 Captcha Interceptor

@Component
public class CaptchaInterceptor implements HandlerInterceptor {
    @Autowired
    private CaptchaService captchaService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) return true;
        HandlerMethod hm = (HandlerMethod) handler;
        CaptchaRequired cr = hm.getMethodAnnotation(CaptchaRequired.class);
        if (cr == null) return true;
        String code = request.getParameter(cr.captchaParam());
        if (StringUtils.hasText(code) && captchaService.validateCaptcha(request, code)) {
            return true;
        }
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        response.getWriter().write("{\"code\":400,\"message\":\"Invalid or expired captcha\"}");
        return false;
    }
}

5.6 Behavior Analysis Service

@Service
@Slf4j
public class BehaviorAnalysisService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    public boolean isSuspicious(HttpServletRequest request) {
        String ip = getIpAddress(request);
        // Frequency check
        String freqKey = "behavior:freq:" + ip;
        Long count = redisTemplate.opsForValue().increment(freqKey, 1);
        redisTemplate.expire(freqKey, 1, TimeUnit.MINUTES);
        if (count != null && count > 30) {
            log.warn("High request frequency from IP {}: {}", ip, count);
            return true;
        }
        // User‑Agent check
        String ua = request.getHeader("User-Agent");
        if (ua == null || isBotUserAgent(ua)) {
            log.warn("Suspicious User-Agent: {}", ua);
            return true;
        }
        // Uniform interval check (simplified)
        String timeKey = "behavior:time:" + ip;
        long now = System.currentTimeMillis();
        String lastStr = redisTemplate.opsForValue().get(timeKey);
        if (lastStr != null) {
            long interval = now - Long.parseLong(lastStr);
            if (isUniformInterval(ip, interval)) {
                log.warn("Uniform request interval from IP {}: {} ms", ip, interval);
                return true;
            }
        }
        redisTemplate.opsForValue().set(timeKey, String.valueOf(now), 10, TimeUnit.MINUTES);
        return false;
    }
    private boolean isBotUserAgent(String ua) {
        String lower = ua.toLowerCase();
        return lower.contains("bot") || lower.contains("spider") || lower.contains("crawl") || lower.isEmpty() || lower.length() < 40;
    }
    private boolean isUniformInterval(String ip, long interval) {
        String key = "behavior:intervals:" + ip;
        List<String> recent = redisTemplate.opsForList().range(key, 0, 4);
        redisTemplate.opsForList().leftPush(key, String.valueOf(interval));
        redisTemplate.opsForList().trim(key, 0, 9);
        redisTemplate.expire(key, 10, TimeUnit.MINUTES);
        if (recent == null || recent.size() < 5) return false;
        List<Long> intervals = recent.stream().map(Long::parseLong).collect(Collectors.toList());
        double mean = intervals.stream().mapToLong(Long::longValue).average().orElse(0);
        double variance = intervals.stream().mapToDouble(i -> Math.pow(i - mean, 2)).average().orElse(0);
        return variance < 100; // threshold may be tuned
    }
    // getIpAddress omitted for brevity
}

5.7 Behavior Analysis Interceptor

@Component
public class BehaviorAnalysisInterceptor implements HandlerInterceptor {
    @Autowired
    private BehaviorAnalysisService analysisService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String path = request.getRequestURI();
        if (path.startsWith("/api/") && isRiskEndpoint(path)) {
            if (analysisService.isSuspicious(request)) {
                response.setContentType("application/json;charset=UTF-8");
                response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
                response.getWriter().write("{\"code\":429,\"message\":\"Suspicious activity detected, please verify\",\"needCaptcha\":true}");
                return false;
            }
        }
        return true;
    }
    private boolean isRiskEndpoint(String path) {
        return path.contains("/login") || path.contains("/register") || path.contains("/payment") || path.contains("/order") || path.contains("/password");
    }
}

Pros and Cons

Effective bot mitigation : Captcha blocks automated scripts, behavior analysis catches sophisticated bots.

Higher user friction : Additional steps may degrade user experience.

Implementation complexity : Requires coordination between front‑end and back‑end.

Potential false positives : Aggressive analysis might block legitimate users.

Conclusion

API anti‑scraping is a systemic challenge that must balance security, performance, and usability. The five solutions presented—annotation limits, token bucket, Redis‑Lua distributed limits, Sentinel, and captcha/behavior analysis—each have distinct strengths and trade‑offs. Selecting or combining the appropriate techniques based on interface criticality, traffic patterns, and deployment topology will help build robust, user‑friendly services.

Key principles to follow:

Minimal impact : Protect without degrading normal user experience.

Layered defense : Apply stronger measures to high‑risk endpoints.

Observability : Monitor and alert on abnormal traffic.

Flexibility : Enable dynamic adjustment of limits and rules as conditions evolve.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.