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.53k stars 3.33k forks source link

Spring Cloud Gateway - How to implement Rate Limiter not by Key, but by Class of User #3578

Open dreamstar-enterprises opened 4 days ago

dreamstar-enterprises commented 4 days ago

Hi,

I'm currently trying to implement a Rate Limiter on my AWS infrastructure. Requests for my app currently go through Spring Cloud Gateway (which does circuit breaking, retries, rate limiting, and a timeout)

My rate limiter code looks like this:

@Configuration
internal class RequestRateLimiterConfig(
    private val requestRateLimiterGatewayFilterFactory: RequestRateLimiterGatewayFilterFactory,
    private val redisRateLimiter: RedisRateLimiter,
    private val defaultKeyResolver: KeyResolver
) {

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

    @Bean
    fun requestRateLimiter(): GlobalFilter {

        // rate limiter filter
        val rateLimiterConfig = RequestRateLimiterGatewayFilterFactory.Config().apply {
            rateLimiter = redisRateLimiter
            keyResolver = defaultKeyResolver
            denyEmptyKey = true
            statusCode = HttpStatus.TOO_MANY_REQUESTS
            emptyKeyStatus = HttpStatus.BAD_REQUEST.name
        }

        val rateLimiterFilter = requestRateLimiterGatewayFilterFactory.apply(rateLimiterConfig)

        return GlobalFilter { exchange, chain ->
            val keyMono = defaultKeyResolver.resolve(exchange)
            keyMono
                .flatMap { key ->
                    if (key.isNullOrEmpty()) {
                        // if key is null or empty, return error
                        logger.warn("Empty session ID detected. Sending error response.")
                        return@flatMap LocalExceptionHandlers.missingKey(exchange)
                    } else {
                        // if key is present, continue with rate limiting
                        logger.info("Resolved key: $key")
                        return@flatMap rateLimiterFilter.filter(exchange, chain)
                            .onErrorResume { e ->
                                // Handle rate limiting errors and send error response
                                val status = exchange.response.statusCode
                                if (status == HttpStatus.TOO_MANY_REQUESTS) {
                                    return@onErrorResume LocalExceptionHandlers.rateLimitExceeded(exchange)
                                }
                                Mono.error(e)
                            }
                    }
                }.then()
        }
    }
}
    /**
     * Redis Rate Limited
     */
    @Bean
    fun redisRateLimiter(): RedisRateLimiter {
        return RedisRateLimiter(10, 20, 1)
    }
    /**
     * Default Key Resolver
     */
    @Bean
    fun defaultKeyResolver(): KeyResolver {
        return KeyResolver { exchange: ServerWebExchange ->
            val sessionId = exchange.request.cookies[sessionProperties.SESSION_COOKIE_NAME]?.first()?.value
            if (sessionId.isNullOrBlank()) {
                logger.warn("No session ID found in cookie.")
                Mono.just("")
            } else {
                logger.info("Resolved session ID for Rate Limiting: $sessionId")
                Mono.justOrEmpty(sessionId)
            }
        }
    }

It is fairly simple with rate limiting done by a key present in the request (in my case session cookie id)

Now, what if I wanted to have different rate limits, by "classes of users" e.g. (premium, basic, admin_user, normal_user) How would I do that using SCG? I tried exploring AWS API Gateway as they have something called Usage Plans for Rate limiting, but it didn't make sense having two Gateways, AWS API Gateway + Spring Cloud Gateway

My SCG does do authorization, so does get an access token from Auth0 by Okta, and store that in redis.

In theory, like API Gateway has a Lambda Authorizer, I could verify the token in the SCG, to get e.g. the ROLE of a user, and based on that use different rate limiters.

But I have been told that verifying an access token in both the SCG, and Spring Rest API behind it, is bad practice. (currently the access token sent by the SCG app is verified by the Spring Rest API only (using Spring Security Resource Server. SCG uses Spring Security Client, to get the access token only from Auth0 by Okta)

Or should I just try to use AWS API Gateway?

Any help to the above would be appreciated

spencergibb commented 4 days ago

Now, what if I wanted to have different rate limits, by "classes of users" e.g. (premium, basic, admin_user, normal_user)

That would be something you implement in your key relsover, we have no way of doing that.

dreamstar-enterprises commented 4 days ago

Ok I see. Would this not need extra information, e.g. from the access token it receives from Auth0, to know what class of user, the request is from (E.g. premium, basic, etc.) So I would have to verify the token in the Spring Cloud Gateway, to extract that information, in addition to verifying it, in the backend rest api microservice?

spencergibb commented 4 days ago

That's all specific to your application and I have no way of helping with it.

dreamstar-enterprises commented 4 days ago

Ok - I wonder how other companies do it. I'll look into it. I'm assuming I would have only "one instance" of the spring cloud gateway, even for a distributed system (that needed to horizontally scale, as request traffic increased)