Implementing Interface Rate Limiting with Spring Interceptor and Redis

This article demonstrates how to build a Spring‑based API rate‑limiting solution using an Interceptor and Redis, covering the basic principle, project setup, code implementation, custom annotation with reflection for flexible limits, handling path‑parameter keys, real‑IP acquisition, and practical considerations for production use.

Architecture Digest
Architecture Digest
Architecture Digest
Implementing Interface Rate Limiting with Spring Interceptor and Redis

The article introduces a demo that prevents API abuse by combining a Spring HandlerInterceptor with Redis to count requests per IP and URI, then blocks excessive calls.

Principle : The client IP and request URI are concatenated to form a unique key. The interceptor checks Redis for a lock key; if none exists, it increments a counter key with a configurable expiration. When the counter exceeds the allowed threshold, a lock key is set and further requests are rejected.

Project address : The full source code is available at https://github.com/Tonciy/interface-brush-protection . The most important logic resides in the AccessLimintInterceptor class.

Initial implementation:

/**
 * @author: Zero
 * @time: 2023/2/14
 * @description: 接口防刷拦截处理
 */
@Slf4j
public class AccessLimintInterceptor implements HandlerInterceptor {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Value("${interfaceAccess.second}")
    private Long second = 10L;
    @Value("${interfaceAccess.time}")
    private Long time = 3L;
    @Value("${interfaceAccess.lockTime}")
    private Long lockTime = 60L;
    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)) {
                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 configuration values ( second, time, lockTime) are supplied via external properties, allowing dynamic adjustment.

Enhancement – custom annotation + reflection : To support different limits per endpoint, a custom @AccessLimit annotation is defined. The interceptor first checks the annotation on the handler method; if absent, it checks the controller class. Annotation values override the global defaults, enabling per‑method or per‑class rate limits.

/**
 * @author: Zero
 * @time: 2023/2/14
 * @description: 接口防刷拦截处理 (version 2.0)
 */
@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;
            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();
            }
            String ip = request.getRemoteAddr();
            String uri = request.getRequestURI();
            if ((methodAnno != null) || classLevel) {
                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;
    }
}

Using the annotation on a controller or method makes the rate‑limit configuration explicit and avoids hard‑coding values in the interceptor.

Path‑parameter handling : When URIs contain variable segments (e.g., /pass/{id}), the raw URI differs for each request, causing the interceptor to treat them as distinct endpoints. The article suggests either avoiding path parameters for rate‑limited APIs or replacing the URI with a stable identifier such as ClassName#MethodName.

Real IP acquisition : The simple request.getRemoteAddr() may return the proxy’s address. The author notes that extracting the true client IP from headers like X‑Forwarded‑For is necessary in production environments.

Conclusion : The author reflects on the learning journey, from a basic counter‑based limiter to a flexible annotation‑driven solution, and mentions related concepts such as singleton patterns, reflection, thread safety, and JVM internals that were explored along the way.

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.

BackendJavaredisspringInterceptorrate limiting
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

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.