Implementing Interface Rate Limiting with Spring Interceptor and Redis in Java

This article demonstrates how to prevent API abuse in a Java Spring application by using a custom HandlerInterceptor combined with Redis to track request counts per IP and URI, covering basic implementation, configuration, custom annotations, reflection for flexible limits, and discusses potential pitfalls and improvements.

Architect
Architect
Architect
Implementing Interface Rate Limiting with Spring Interceptor and Redis in Java

1. Introduction

This article describes a demo that implements API rate‑limiting using a Spring HandlerInterceptor and Redis. The goal is to prevent excessive requests to an interface by counting accesses per IP address and URI.

2. Principle

Combine the client IP and request URI as a unique key to identify a visitor’s request.

Intercept each request in the preHandle method, read the counter from Redis, and decide whether to allow the request or block it.

The following diagram (omitted) illustrates the workflow.

3. Project Resources

Project repository: https://github.com/Tonciy/interface-brush-protection

Apifox documentation (password: Lyh3j2Rv) is also provided.

4. Core Interceptor Code

@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 {
        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("首次访问");
                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 time window, maximum request count, and lock duration are configurable via the application’s properties file.

5. Self‑Questioning

The basic implementation works, but it applies the same x seconds, y requests, and z lock time to all protected endpoints, which is not flexible enough for real projects.

5.1 Interceptor Mapping Rules

Spring allows interceptors to be mapped to specific URL patterns. By configuring the interceptor to match only the endpoints that need protection, we can limit its scope.

5.2 Custom Annotation + Reflection

Define a custom annotation @AccessLimit with attributes second, maxTime, and forbiddenTime. Apply the annotation either on controller methods or on the whole controller class. In the interceptor, use reflection to read the annotation values and enforce per‑endpoint 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;
            AccessLimit methodAnno = targetMethod.getMethodAnnotation(AccessLimit.class);
            long second, maxTime, forbiddenTime;
            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
            }
            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;
    }
}

This approach allows each endpoint to have its own rate‑limit parameters, and class‑level annotations provide a convenient way to apply the same limits to all methods in a controller.

6. Remaining Issues

Time‑window accuracy: The simple Redis key expiration method treats the first request as the start of the window, which may not reflect a true sliding‑window algorithm.

Path parameters: When the same endpoint is accessed with different path variables, the URI differs, causing separate counters. Solutions include ignoring path variables or using the method name (and class name) as the key.

Real IP acquisition: request.getRemoteAddr() may return the proxy address; extracting the client IP from headers (e.g., X‑Forwarded‑For) is necessary in production.

7. Summary

The article walks through a complete implementation of API rate limiting in a Spring MVC project, starting from a basic interceptor‑Redis solution, evolving to a flexible annotation‑based design, and finally discussing practical concerns such as sliding windows, path parameters, and real‑IP handling.

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.

JavaReflectionspringInterceptorannotations
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.