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.
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-application4.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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
