스프링 시큐리티 API Rate Limit

조승빈·2024년 11월 1일

Spring Security

목록 보기
11/11
post-thumbnail

Rate Limiting 구현 방법

  • Fixed Window (고정 창): 일정 시간 간격 내에서 요청을 제한한다. 예: 1분에 100회 요청 제한.
  • Sliding Window (슬라이딩 창): 일정 시간 내 요청을 허용하되, 연속적인 시간이 아닌 롤링 타임 윈도우를 사용해 좀 더 부드럽게 제한한다.
  • Token Bucket (토큰 버킷): 주기적으로 토큰을 채우고, 요청이 있을 때마다 토큰을 사용하여 요청 속도를 제어한다.
  • Leaky Bucket (누수 버킷): 급증하는 요청을 처리하되, 일관된 속도로 처리 속도를 제한하여 서버 과부하를 방지한다.

Bucket4j를 선택한 이유

  • JVM 친화적인 효율성: Bucket4j는 Java 환경에서 최적화되어 성능이 뛰어나다.
  • 유연한 토큰 기반 모델: 버스트 요청 처리 및 다양한 리필 전략을 지원하여 유연한 속도 제한 설정이 가능하다.
  • 분산 환경 지원: Redis나 Hazelcast 같은 분산 캐시와 통합 가능해 다중 인스턴스 환경에서 확장성이 뛰어나다.
    메모 어플이다보니 나중에 캐시 서버를 추가할 것을 고려해서 Bucket4j가 적절할 것이라고 생각했다.

Bucket4j를 사용한 Spring Boot에 Rate Limiting 적용 과정

Rate Limit 설정을 위한 RateLimitConfig 클래스

요청 제한을 정의하는 Bucket을 설정하는 RateLimitConfig 클래스를 추가한다.
1분당 10회 요청을 허용하도록 하고 빈으로 관리한다.

@Configuration
class RateLimitConfig {

    @Bean
    fun rateLimitBucket(): Bucket {
        // 1분당 최대 10회의 요청을 허용
        val limit = Bandwidth.classic(10, Refill.greedy(10, Duration.ofMinutes(1)))
        return Bucket4j.builder().addLimit(limit).build()
    }
}

요청을 필터링하는 RateLimitFilter 클래스

요청이 올 때마다 Bucket에서 토큰을 소모하며, 토큰이 부족할 경우 접근을 차단하는 RateLimitFilter를 추가한다.
각 요청마다 bucket.tryConsume(1)을 호출하여 토큰을 소모한다. 남은 토큰이 없을 경우 403 Forbidden을 반환한다.

@Component
class RateLimitFilter(private val bucket: Bucket) : OncePerRequestFilter() {

    @Throws(ServletException::class, IOException::class)
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        if (bucket.tryConsume(1)) {
            filterChain.doFilter(request, response)
        } else {
            response.status = HttpServletResponse.SC_FORBIDDEN
            response.writer.write("Too many requests - try again later")
        }
    }
}

SecurityConfig에 추가

@Configuration
@EnableWebSecurity
class SecurityConfig(
    private val jwtTokenProvider: JwtTokenProvider,
    private val rateLimitFilter: RateLimitFilter,
    @Value("\${cors.allowed.origins}") private val allowedOrigins: List<String>
) {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http
            // ... 이전 코드
            .addFilterBefore(
                rateLimitFilter,  // RateLimitFilter 추가
                SecurityContextHolderAwareRequestFilter::class.java
            )
            // ... 이전 코드
        return http.build()
    }

    @Bean
    fun customAuthenticationEntryPoint(): AuthenticationEntryPoint {
        return CustomAuthenticationEntryPoint()
    }

    // ... 이전 코드
}
profile
평범

0개의 댓글