How to Build a Flexible API Rate‑Limiting System with Spring Interceptor and Redis
This article demonstrates step‑by‑step how to prevent API abuse in a Spring MVC application by using an Interceptor combined with Redis for counting requests, customizing limits per endpoint through annotations and reflection, handling mapping rules, path‑parameter challenges, and obtaining the real client IP.
Principle
Combine the client IP address and request URI to uniquely identify a caller.
Intercept each request with a Spring HandlerInterceptor, store the request count in Redis, and block the caller when the configured threshold is exceeded.
Project Setup
Project repository: https://github.com/Tonciy/interface-brush-protection
Apifox documentation: https://apifox.com/apidoc/shared-4993f98d-722c-46b3-be76-9e4eb04d2fd3 (password: Lyh3j2Rv)
The most important logic resides in the Interceptor:
/**
* @author Zero
* @time 2023/2/14
* @description Interface rate‑limiting interceptor
*/
@Slf4j
public class AccessLimintInterceptor implements HandlerInterceptor {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/** lock key prefix */
public static final String LOCK_PREFIX = "LOCK";
/** count key prefix */
public static final String COUNT_PREFIX = "COUNT";
@Value("${interfaceAccess.second}")
private Long second = 10L; // time window (seconds)
@Value("${interfaceAccess.time}")
private Long time = 3L; // max accesses
@Value("${interfaceAccess.lockTime}")
private Long lockTime = 60L; // lock duration (seconds)
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
String ip = request.getRemoteAddr(); // real IP 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("First visit");
redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
} else if ((Integer) count < time) {
redisTemplate.opsForValue().increment(countKey);
} else {
log.info("{} locked for {}", 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;
}
}Custom Annotation + Reflection (Version 2.0)
Define an annotation @AccessLimit that carries second, maxTime and forbiddenTime. Apply it either on a controller method or on the whole controller class. The interceptor reads the annotation via reflection, giving each endpoint its own limits.
/**
* @author Zero
* @time 2023/2/14
* @description Interface rate‑limiting interceptor (annotation version)
*/
@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;
// Class‑level annotation
AccessLimit classAnno = targetMethod.getMethod().getDeclaringClass().getAnnotation(AccessLimit.class);
boolean classLevel = classAnno != null;
// Method‑level annotation
AccessLimit methodAnno = targetMethod.getMethodAnnotation(AccessLimit.class);
long second = 0L, maxTime = 0L, forbiddenTime = 0L;
if (methodAnno != null) {
second = methodAnno.second();
maxTime = methodAnno.maxTime();
forbiddenTime = methodAnno.forbiddenTime();
} else if (classLevel) {
second = classAnno.second();
maxTime = classAnno.maxTime();
forbiddenTime = classAnno.forbiddenTime();
} else {
return true; // no rate‑limit needed
}
String ip = request.getRemoteAddr();
String uri = request.getRequestURI();
if (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)) {
redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
} else if ((Integer) count < maxTime) {
redisTemplate.opsForValue().increment(countKey);
} else {
redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);
redisTemplate.delete(countKey);
return true;
}
} else {
return true;
}
return false;
}
}Mapping‑Rule Approach
Instead of annotating every method, configure the interceptor’s URL patterns so that only the desired endpoints are subject to rate limiting. This reduces maintenance when many endpoints share the same limits.
Sliding‑Window Time Logic Issue
The simple fixed‑window implementation (using Redis key expiration) may allow bursts that violate the strict “x seconds within y requests” rule. A true sliding‑window algorithm would need to store timestamps of each request and evaluate the count over any continuous interval.
Path‑Parameter Problem
When the URI contains variable path parameters, the key ip+uri treats each distinct URI as a different endpoint, causing the limiter to miss repeated calls to the same logical method. Solutions:
Avoid path parameters for rate‑limited endpoints.
Replace the raw URI with a stable identifier such as controllerClassName#methodName, optionally combined with the HTTP method.
Obtaining the Real Client IP
Behind proxies, request.getRemoteAddr() returns the proxy’s address. Use standard headers like X‑Forwarded‑For or X‑Real‑IP to retrieve the original client IP.
Conclusion
The tutorial walks through building a basic rate‑limiting interceptor, evolving it to a flexible, annotation‑driven solution, discusses practical pitfalls (time‑window accuracy, path parameters, real IP), and shows how each improvement deepens understanding of Spring MVC, Redis, reflection, and distributed throttling techniques.
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.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
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.
