Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
dreamstar-enterprises opened this issue Nov 1, 2024 · 0 comments

Comments

@dreamstar-enterprises
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant