본문 바로가기
Java & Kotlin/Spring

Request Rate Limiting with Spring Cloud Gateway - 부록. custom Filter 만들기

by devson 2022. 2. 8.

기본으로 제공되는 RequestRateLimiter 필터를 사용하게 되면 사용률 제한에 걸리지 않은 요청에 대해서는 문제없지만,

제한이 걸린 요청에 대해 우리 서비스 내부적으로 사용하는 응답 포맷을 사용할 수 없다.

 

그렇기 때문에 응답 포맷을 항상 지정된 형태로 주어야한다면 RequestRateLimiter를 그대로 사용하기는 힘들고,

custom filter를 만들어서 이를 RequestRateLimiter 대신 사용해야할 것이다.

 

 

아래는 RedisRateLimiter를 사용하는 custom 사용률 제한 필터의 예제 코드다. (링크)

여기서 Spring Cloud Gateway의 RateLimiter를 사용하기위해 Config 클래스는 HasRouteId 를 상속받아 Router의 ID를 받을 수 있도록 하였다.

(우리가 따로 set하는 코드를 작성할 필요는 없다)

 

package com.tistory.devs0n.gateway.filters.ratelimit

import org.springframework.cloud.gateway.filter.GatewayFilter
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter
import org.springframework.cloud.gateway.support.HasRouteId
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.server.reactive.ServerHttpResponse
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono

@Component
class RequestRateLimitFilter(
    private val rateLimiter: RateLimiter<RedisRateLimiter.Config>,
) : AbstractGatewayFilterFactory<RequestRateLimitFilter.Config>(Config::class.java) {
    class Config(
        val keyResolver: KeyResolver,
    ) : HasRouteId {
        private var routeId: String? = null

        override fun getRouteId(): String = this.routeId!!

        override fun setRouteId(routeId: String) {
            this.routeId = routeId
        }
    }

    override fun apply(config: Config): GatewayFilter {
        return GatewayFilter { exchange, chain ->
            val response = exchange.response
            val keyResolver = config.keyResolver
            val routeId = config.routeId

            return@GatewayFilter keyResolver.resolve(exchange)
                .flatMap { key ->
                    return@flatMap this.rateLimiter.isAllowed(routeId, key)

                }.flatMap { rateLimitResponse ->
                    return@flatMap when (rateLimitResponse.isAllowed) {
                        true -> chain.filter(exchange)
                        false -> this.responseTooManyRequest(response)
                    }
                }
        }
    }

    private fun responseTooManyRequest(response: ServerHttpResponse): Mono<Void> {
        response.statusCode = HttpStatus.TOO_MANY_REQUESTS
        response.headers.contentType = MediaType.APPLICATION_JSON
        return response.writeWith(
            Mono.just(
                response.bufferFactory().wrap(
                    """{
                        |"success": false,
                        |"message":"You sent too many requests"
                    |}""".trimMargin().toByteArray()
                )
            )
        )
    }
}

 

 

RedisRateLimiter를 그대로 사용하기 때문에 관련 application.yml에 설정을 그대로 사용하면되고,

filter의 name만 변경해주면 그대로 적용가능하다.

server:
  port: 9000

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0

  cloud:
    gateway:
      default-filters:
        - name: AuthFilter
          args:
            whiteList:
              chrisToken: chris

      routes:
        - id: all
          uri: http://localhost:8080
          predicates:
            - Path=/**

          filters:
#            - name: RequestRateLimiter
            - name: RequestRateLimitFilter
              args:
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 1
                redis-rate-limiter.requestedTokens: 1
#                redis-rate-limiter.replenishRate: 20
#                redis-rate-limiter.burstCapacity: 100
#                redis-rate-limiter.requestedTokens: 3
                key-resolver: "#{@userIdAsKeyResolver}"

 

댓글