spring-cloud / spring-cloud-gateway

An API Gateway built on Spring Framework and Spring Boot providing routing and more.
http://cloud.spring.io
Apache License 2.0
4.55k stars 3.33k forks source link

Spring Cloud Gateway - Rate Limiter was working in Spring BFF, but not triggering in Spring Rest-API #3582

Open dreamstar-enterprises opened 3 weeks ago

dreamstar-enterprises commented 3 weeks ago

I took my rate limiter out of my Spring BFF, and put it into my Spring Rest-API as I wanted it to be user specific (and apply different rate limits, depending on the class of user)

So I created my rate limiter.

Rate Limiter

@Component
internal class RequestRateLimiterConfig(
    private val redisRateLimiter: RedisRateLimiter,
    private val defaultKeyResolver: KeyResolver
) : AbstractGatewayFilterFactory<RequestRateLimiterConfig.Config>(Config::class.java) {

    companion object {
        const val RATE_LIMITER_ID = "redis-rate-limiter"
    }

    private val logger = LoggerFactory.getLogger(RequestRateLimiterConfig::class.java)

    override fun apply(config: Config): GatewayFilter {
        return GatewayFilter { exchange: ServerWebExchange, chain ->
            logger.info("Entering rate limiter filter...")
            val keyMono = defaultKeyResolver.resolve(exchange)

            keyMono
                .flatMap { key ->
                    if (key.isNullOrEmpty()) {
                        logger.warn("No key resolved. Blocking request.")
                        LocalExceptionHandlers.missingKey(exchange)
                    } else {
                        logger.info("Resolved key: $key")
                        redisRateLimiter.isAllowed(RATE_LIMITER_ID, key)
                            .flatMap { response ->
                                if (!response.isAllowed) {
                                    logger.warn("Rate limit exceeded for key: $key")
                                    LocalExceptionHandlers.rateLimitExceeded(exchange)
                                } else {
                                    logger.info("Rate limit allowed for key: $key")
                                    chain.filter(exchange)
                                }
                            }
                    }
                }.then()
        }
    }

    override fun newConfig(): Config {
        return Config()
    }

    class Config
}

/**
 * More Rate Limiter configuration
 */
@Configuration
internal class RedisRateLimiterConfig(
    private val authorizationProperties: AuthorizationProperties,
) {

    private val logger = LoggerFactory.getLogger(RedisRateLimiterConfig::class.java)

    /**
     * Redis Rate Limited
     */
    @Bean
    fun redisRateLimiter(): RedisRateLimiter {
        return RedisRateLimiter(1, 1, 1)
    }

    /**
     * Default Key Resolver
     */
    @Bean
    fun defaultKeyResolver(): KeyResolver {
        return KeyResolver { exchange: ServerWebExchange ->

            // fetch authentication context and resolve provider ID if available
            ReactiveSecurityContextHolder.getContext()
                .doOnNext { context -> logger.info("Security context found: ${context.authentication}") }
                .mapNotNull { it.authentication as? JwtAuthenticationToken }
                .flatMap { authentication ->
                    val providerId = authentication?.token?.getClaimAsString(authorizationProperties.authProviderSubjectClaim)
                    if (providerId.isNullOrBlank()) {
                        logger.warn("No provider ID found in token claims.")
                        Mono.just("")
                    } else {
                        logger.info("Resolved provider ID for Rate Limiting: $providerId")
                        Mono.just(providerId)
                    }
                }
                .switchIfEmpty(
                    Mono.defer {
                        logger.warn("No authentication found in security context.")
                        Mono.just("")
                    }
                )
        }
    }

}

But nothing was triggering, I could not see any logs.

So I added this (so all routes are routed to this same server).

Note, I do not think this is ideal in a load balancer environment, as the request has already reached the resource server, and this is effectively forwarding it back out to itself.

But even with this, I get no logged information. I send a request to a hello world endpoint, and I get all 25 back very quickly in < 1 second (when the rate limiter should really kick in after 1 request)

Route Builder

@Configuration
internal class RoutingConfig(
    private val serverProperties: ServerProperties,
    private val rateLimitingFilter: RequestRateLimiterConfig
) {
    private val logger = LoggerFactory.getLogger(RoutingConfig::class.java)

    @Bean
    fun routeLocator(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            .route("resource-server") { r ->
                r.path("/**") // Match all paths
                    .filters { f ->
                        logger.info("Entering resource-server route")
                        f.filter(rateLimitingFilter.apply(rateLimitingFilter.newConfig()))
                    }
                    .uri(serverProperties.resourceServerUri)
            }
            .build()
    }
}

Yaml

Here is my yaml config

# default spring settings
spring:
  # spring cloud settings
  cloud:
    # spring gateway settings
    gateway:
      metrics:
        enabled: true
        tags:
          path:
            enabled: true

OAuth Resource Server

And my resource server security chain is quite standard:

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity(useAuthorizationManager = true)
internal class ResourceSecurityConfig() {
    @Bean
    /* security filter chain for authentication & authorization (reactive) */
    /* this should be webSession stateless */
    fun resourceServerSecurityFilterChain(
        http: ServerHttpSecurity,
      ): SecurityWebFilterChain {

          /* enable csrf */
            http.csrf { csrf ->
                csrf.disable()
            }

          /* oauth2.0 resource server */
            http.oauth2ResourceServer { oauth2 ->
                oauth2.authenticationManagerResolver(authenticationManagerResolver)
                oauth2.authenticationEntryPoint(authenticationEntryPoint)
                oauth2.accessDeniedHandler(accessDeniedHandler)
            }

           /* configure authorization  */
            http.authorizeExchange { authorize ->
                authorize
                    .pathMatchers(
                        "/v3/api-docs",
                        "/swagger-ui.html",
                        "/webjars/swagger-ui/**",
                        "/actuator/**").permitAll()
                    .anyExchange().permitAll()
            }

       return http.build()
    }

I'm not sure where I've gone wrong? Maybe its the yaml?

Would appreciate any help