InterceptorConfig.java

package org.darkroomlibrary.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.darkroomlibrary.infrastructure.cache.CacheService;
import org.darkroomlibrary.infrastructure.security.ClientIpResolver;
import org.darkroomlibrary.infrastructure.security.UserAuthLookup;
import org.darkroomlibrary.interceptor.JwtInterceptor;
import org.darkroomlibrary.interceptor.RateLimitInterceptor;
import org.darkroomlibrary.interceptor.TokenBucketRateLimitInterceptor;
import org.darkroomlibrary.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.time.Duration;

/**
 * Configures authentication and request-rate interceptors.
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    private static final String FILE_GET_PATH = "/file/getFile";
    private static final String FILE_PUBLIC_PATH = "/file/public";
    private static final String HEALTH_LIVE_PATH = "/health/live";
    private static final String HEALTH_READY_PATH = "/health/ready";

    private final String apiPrefix;
    private final ClientIpResolver clientIpResolver;
    private final int ingressMaxRequestsPerMinute;
    private final int publicFileMaxRequestsPerMinute;
    private final int anonymousMaxRequestsPerMinute;
    private final int authenticatedMaxRequestsPerMinute;
    private final int loginBucketCapacity;
    private final int accountBucketCapacity;
    private final int verificationBucketCapacity;
    private final int captchaBucketCapacity;
    private final CacheService cacheService;
    private final UserAuthLookup userAuthLookup;
    private final ObjectMapper objectMapper;
    private final JwtUtil jwtUtil;

    public InterceptorConfig(
            @Value("${app.api-prefix}") String apiPrefix,
            @Value("${security.rate-limit.ingress-max-per-minute:1200}") int ingressMaxRequestsPerMinute,
            @Value("${security.rate-limit.public-file-max-per-minute:600}") int publicFileMaxRequestsPerMinute,
            @Value("${security.rate-limit.anonymous-max-per-minute:60}") int anonymousMaxRequestsPerMinute,
            @Value("${security.rate-limit.authenticated-max-per-minute:300}") int authenticatedMaxRequestsPerMinute,
            @Value("${security.sensitive-rate-limit.login-capacity:60}") int loginBucketCapacity,
            @Value("${security.sensitive-rate-limit.account-capacity:12}") int accountBucketCapacity,
            @Value("${security.sensitive-rate-limit.verification-capacity:6}") int verificationBucketCapacity,
            @Value("${security.sensitive-rate-limit.captcha-capacity:90}") int captchaBucketCapacity,
            CacheService cacheService,
            ClientIpResolver clientIpResolver,
            UserAuthLookup userAuthLookup,
            ObjectMapper objectMapper,
            JwtUtil jwtUtil) {
        this.apiPrefix = apiPrefix;
        this.clientIpResolver = clientIpResolver;
        this.ingressMaxRequestsPerMinute = ingressMaxRequestsPerMinute;
        this.publicFileMaxRequestsPerMinute = publicFileMaxRequestsPerMinute;
        this.anonymousMaxRequestsPerMinute = anonymousMaxRequestsPerMinute;
        this.authenticatedMaxRequestsPerMinute = authenticatedMaxRequestsPerMinute;
        this.loginBucketCapacity = loginBucketCapacity;
        this.accountBucketCapacity = accountBucketCapacity;
        this.verificationBucketCapacity = verificationBucketCapacity;
        this.captchaBucketCapacity = captchaBucketCapacity;
        this.cacheService = cacheService;
        this.userAuthLookup = userAuthLookup;
        this.objectMapper = objectMapper;
        this.jwtUtil = jwtUtil;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RateLimitInterceptor(
                        clientIpResolver,
                        ingressMaxRequestsPerMinute,
                        ingressMaxRequestsPerMinute,
                        cacheService,
                        objectMapper,
                        "ingress",
                        RateLimitInterceptor.SubjectMode.IP_ONLY))
                .addPathPatterns("/**")
                .excludePathPatterns(
                        HEALTH_LIVE_PATH,
                        HEALTH_READY_PATH
                )
                .order(0);

        registry.addInterceptor(new JwtInterceptor(apiPrefix, userAuthLookup, objectMapper, jwtUtil))
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/user/login",
                        "/user/register",
                        "/user/resetPwd",
                        "/user/sendVerifyCode",
                        FILE_GET_PATH,
                        FILE_PUBLIC_PATH,
                        "/captcha/generate",
                        "/captcha/verify",
                        HEALTH_LIVE_PATH,
                        HEALTH_READY_PATH,
                        "/error"
                )
                .order(1);

        registry.addInterceptor(tokenBucket(
                        "login",
                        TokenBucketRateLimitInterceptor.SubjectMode.IP_ONLY,
                        loginBucketCapacity,
                        Duration.ofMinutes(1)))
                .addPathPatterns("/user/login")
                .order(2);

        registry.addInterceptor(tokenBucket(
                        "account",
                        TokenBucketRateLimitInterceptor.SubjectMode.IP_ONLY,
                        accountBucketCapacity,
                        Duration.ofMinutes(10)))
                .addPathPatterns("/user/register", "/user/resetPwd")
                .order(2);

        registry.addInterceptor(tokenBucket(
                        "verification",
                        TokenBucketRateLimitInterceptor.SubjectMode.AUTHENTICATED_OR_IP,
                        verificationBucketCapacity,
                        Duration.ofMinutes(10)))
                .addPathPatterns("/user/sendVerifyCode", "/user/sendEmailChangeCode")
                .order(2);

        registry.addInterceptor(tokenBucket(
                        "captcha",
                        TokenBucketRateLimitInterceptor.SubjectMode.IP_ONLY,
                        captchaBucketCapacity,
                        Duration.ofMinutes(1)))
                .addPathPatterns("/captcha/generate", "/captcha/verify")
                .order(2);

        registry.addInterceptor(new RateLimitInterceptor(
                        clientIpResolver,
                        publicFileMaxRequestsPerMinute,
                        publicFileMaxRequestsPerMinute,
                        cacheService,
                        objectMapper,
                        "public-file",
                        RateLimitInterceptor.SubjectMode.IP_ONLY))
                .addPathPatterns(
                        FILE_GET_PATH,
                        FILE_PUBLIC_PATH
                )
                .order(3);

        registry.addInterceptor(new RateLimitInterceptor(
                        clientIpResolver,
                        anonymousMaxRequestsPerMinute,
                        authenticatedMaxRequestsPerMinute,
                        cacheService,
                        objectMapper))
                .addPathPatterns("/**")
                .excludePathPatterns(
                        FILE_GET_PATH,
                        FILE_PUBLIC_PATH,
                        HEALTH_LIVE_PATH,
                        HEALTH_READY_PATH
                )
                .order(4);
    }

    private TokenBucketRateLimitInterceptor tokenBucket(
            String namespace,
            TokenBucketRateLimitInterceptor.SubjectMode subjectMode,
            int capacity,
            Duration refillPeriod) {
        return new TokenBucketRateLimitInterceptor(
                clientIpResolver,
                cacheService,
                objectMapper,
                namespace,
                subjectMode,
                capacity,
                refillPeriod);
    }
}