Mastering API Rate Limiting with Spring Interceptor and Redis
This article walks through building a Spring MVC interceptor that leverages Redis to enforce per‑IP request limits, explains configurable parameters, shows how to apply protection selectively via mapping rules or custom annotations, and discusses practical pitfalls such as sliding‑window logic, path‑parameter handling, and real‑IP detection.
Principle
The rate‑limiting interceptor combines the client IP address and request URI to form a unique Redis key. For each request it checks a lock key; if the client is not locked it increments a counter key within a configurable time window. When the request count exceeds the allowed maximum, a lock key is set for a configurable lock duration and further requests are rejected.
Project Repository
Source code: https://github.com/Tonciy/interface-brush-protection
Version 1 – Basic Interceptor
@Slf4j
public class AccessLimintInterceptor implements HandlerInterceptor {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Value("${interfaceAccess.second}")
private Long second = 10L; // time window (seconds)
@Value("${interfaceAccess.time}")
private Long time = 3L; // max requests
@Value("${interfaceAccess.lockTime}")
private Long lockTime = 60L; // lock duration (seconds)
public static final String LOCK_PREFIX = "LOCK";
public static final String COUNT_PREFIX = "COUNT";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
String ip = request.getRemoteAddr(); // real IP only when no proxy
String lockKey = LOCK_PREFIX + ip + uri;
Object isLock = redisTemplate.opsForValue().get(lockKey);
if (Objects.isNull(isLock)) {
String countKey = COUNT_PREFIX + ip + uri;
Object count = redisTemplate.opsForValue().get(countKey);
if (Objects.isNull(count)) {
log.info("首次访问");
redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
} else {
if ((Integer) count < time) {
redisTemplate.opsForValue().increment(countKey);
} else {
log.info("{}禁用访问{}", ip, uri);
redisTemplate.opsForValue().set(lockKey, 1, lockTime, TimeUnit.SECONDS);
redisTemplate.delete(countKey);
throw new CommonException(ResultCode.ACCESS_FREQUENT);
}
}
} else {
throw new CommonException(ResultCode.ACCESS_FREQUENT);
}
return true;
}
}The three properties ${interfaceAccess.second}, ${interfaceAccess.time} and ${interfaceAccess.lockTime} are defined in the application configuration and can be adjusted without code changes.
Selective Application – Interceptor Mapping
Instead of applying the interceptor to all URLs, configure its path patterns (e.g., /api/**) in WebMvcConfigurer. Only the matched endpoints will be rate‑limited.
Custom Annotation + Reflection (Version 2)
A custom annotation @AccessLimit can specify second, maxTime and forbiddenTime per class or method. The interceptor inspects the annotation via reflection and applies the corresponding limits.
@Slf4j
public class AccessLimintInterceptor implements HandlerInterceptor {
@Resource
private RedisTemplate<String, Object> redisTemplate;
public static final String LOCK_PREFIX = "LOCK";
public static final String COUNT_PREFIX = "COUNT";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod targetMethod = (HandlerMethod) handler;
AccessLimit classAnno = targetMethod.getMethod().getDeclaringClass().getAnnotation(AccessLimit.class);
boolean classLevel = classAnno != null;
String ip = request.getRemoteAddr();
String uri = request.getRequestURI();
long second = 0L, maxTime = 0L, forbiddenTime = 0L;
if (classLevel) {
second = classAnno.second();
maxTime = classAnno.maxTime();
forbiddenTime = classAnno.forbiddenTime();
}
AccessLimit methodAnno = targetMethod.getMethodAnnotation(AccessLimit.class);
if (methodAnno != null) {
second = methodAnno.second();
maxTime = methodAnno.maxTime();
forbiddenTime = methodAnno.forbiddenTime();
if (isForbidden(second, maxTime, forbiddenTime, ip, uri)) {
throw new CommonException(ResultCode.ACCESS_FREQUENT);
}
} else if (classLevel && isForbidden(second, maxTime, forbiddenTime, ip, uri)) {
throw new CommonException(ResultCode.ACCESS_FREQUENT);
}
}
return true;
}
private boolean isForbidden(long second, long maxTime, long forbiddenTime, String ip, String uri) {
String lockKey = LOCK_PREFIX + ip + uri;
Object isLock = redisTemplate.opsForValue().get(lockKey);
if (Objects.isNull(isLock)) {
String countKey = COUNT_PREFIX + ip + uri;
Object count = redisTemplate.opsForValue().get(countKey);
if (Objects.isNull(count)) {
log.info("首次访问");
redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
} else {
if ((Integer) count < maxTime) {
redisTemplate.opsForValue().increment(countKey);
} else {
log.info("{}禁用访问{}", ip, uri);
redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);
redisTemplate.delete(countKey);
return true;
}
}
} else {
return true;
}
return false;
}
}This approach enables different limits for each endpoint without creating multiple interceptors.
Limitations and Considerations
Sliding‑window accuracy : The implementation treats the first request as the start of the window, which can miss bursts that fall within any x ‑second interval. A true sliding window would require storing timestamps (e.g., a Redis sorted set) and counting entries within the moving window.
Path‑parameter handling : Using the raw URI as part of the Redis key distinguishes requests with different path variables (e.g., /pass/1 vs /pass/2) even though they map to the same logical method. Solutions include ignoring path variables, using the method name, or combining class name and method name as the key.
Real client IP : request.getRemoteAddr() returns the immediate source IP, which may be a proxy. In production, extract the true client IP from headers such as X‑Forwarded‑For or X‑Real‑IP.
Conclusion
The article demonstrates a functional Spring MVC interceptor for API rate limiting backed by Redis, discusses its shortcomings, and shows how to extend it with custom annotations and reflection to achieve per‑endpoint configurability. The incremental improvements illustrate practical use of Spring MVC, Redis, Java reflection, and concurrency concepts in building robust infrastructure components.
Java Architect Handbook
Focused on Java interview questions and practical article sharing, covering algorithms, databases, Spring Boot, microservices, high concurrency, JVM, Docker containers, and ELK-related knowledge. Looking forward to progressing together with you.
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.
