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.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
How to Build a Flexible API Rate‑Limiting System with Spring Interceptor and Redis

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.

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.

JavaRedisReflectionspringInterceptorAnnotationrate limiting
Java High-Performance Architecture
Written by

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.

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.