Elegant API Rate Limiting with Spring Interceptor and Redis
This article demonstrates a step‑by‑step implementation of API anti‑brush (rate limiting) using a Spring Interceptor combined with Redis, explains how to configure time windows and request limits, introduces a custom @AccessLimit annotation for fine‑grained control, discusses path‑parameter pitfalls, real‑IP handling, and shares practical testing results.
Overview
The article demonstrates how to implement API rate‑limiting in a Spring MVC application using a Redis‑backed interceptor. The core idea is to build a Redis key from the client IP and request URI, count requests within a configurable time window, and lock the client for a defined period when the limit is exceeded.
Basic Interceptor Implementation
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 {
String uri = request.getRequestURI();
String ip = request.getRemoteAddr();
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)) {
// first request
redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
} else if ((Integer) count < time) {
// within limit
redisTemplate.opsForValue().increment(countKey);
} else {
// exceed limit – lock client
redisTemplate.opsForValue().set(lockKey, 1, lockTime, TimeUnit.SECONDS);
redisTemplate.delete(countKey);
throw new CommonException(ResultCode.ACCESS_FREQUENT);
}
} else {
// client already locked
throw new CommonException(ResultCode.ACCESS_FREQUENT);
}
return true;
}
}Configurable Limits
The time window ( second), maximum request count ( time) and lock duration ( lockTime) are injected from application.properties, allowing the limits to be changed without recompiling.
Custom Annotation for Fine‑Grained Control
A custom annotation @AccessLimit can be placed on controller classes or methods to specify per‑endpoint limits.
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
long second(); // time window in seconds
long maxTime(); // max requests in the window
long forbiddenTime(); // lock duration in seconds
}The interceptor reads the annotation via reflection; if the annotation is absent the request passes unchanged.
Path Parameter Handling
When an endpoint contains a path variable (e.g., /pass/{id}), using the raw URI as part of the Redis key treats different values as different APIs, breaking the rate‑limit logic. Two approaches are discussed:
Avoid path parameters on rate‑limited endpoints.
Replace the URI with a stable identifier such as controllerClassName + methodName, ensuring all calls to the same method share the same Redis key.
Time‑Window Logic Caveat
The implementation uses a fixed expiration on the counter key, which starts a new window when the key expires. This does not enforce a true sliding‑window rule (any x ‑second interval). For many scenarios this is acceptable, but a strict sliding‑window algorithm would be required for tighter enforcement.
Real IP Retrieval
The code currently obtains the client IP with request.getRemoteAddr(). This works for direct connections but fails when the client is behind a proxy. In such cases the IP should be extracted from headers such as X‑Forwarded‑For.
Testing and Repository
The full source code, including test scenarios for normal access, frequent access, and class‑level annotation, is available at:
https://github.com/Tonciy/interface-brush-protection
Extension with Reflection (Version 2.0)
To achieve true per‑endpoint configurability, the interceptor is enhanced to:
Detect whether the handler is a HandlerMethod.
Read @AccessLimit on the method; if absent, read the annotation on the declaring class.
Apply the annotation values ( second, maxTime, forbiddenTime) accordingly, giving method‑level settings priority over class‑level defaults.
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod targetMethod = (HandlerMethod) handler;
AccessLimit methodAnno = targetMethod.getMethodAnnotation(AccessLimit.class);
AccessLimit classAnno = targetMethod.getMethod().getDeclaringClass().getAnnotation(AccessLimit.class);
long second, maxTime, forbiddenTime;
if (methodAnno != null) {
second = methodAnno.second();
maxTime = methodAnno.maxTime();
forbiddenTime = methodAnno.forbiddenTime();
} else if (classAnno != null) {
second = classAnno.second();
maxTime = classAnno.maxTime();
forbiddenTime = classAnno.forbiddenTime();
} else {
return true; // no rate‑limit defined
}
// same Redis‑based counting logic as before, using the resolved limits
// ... (omitted for brevity)
}
return true;
}This version allows a controller to declare a default limit for all its methods, while individual methods can override the defaults.
Further Considerations
Different endpoints often require different second, maxTime and forbiddenTime values. One solution is to create multiple interceptors, each mapped to a specific set of endpoints, but this increases configuration complexity.
Class‑level @AccessLimit simplifies maintenance: changing the limit for a whole controller requires editing only the annotation on the class.
When using path variables, replace the URI component in the Redis key with a stable identifier (e.g., ClassName_MethodName) to avoid treating each variable value as a separate API.
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.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.
