Вопрос или проблема
Я убрал ограничитель скорости из своего Spring BFF и перенес его в свой Spring Rest-API, так как хотел, чтобы он был специфичным для пользователя (и применял разные ограничения скорости в зависимости от класса пользователя)
Итак, я создал свой ограничитель скорости.
Ограничитель скорости
@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("Вход в фильтр ограничителя скорости...")
val keyMono = defaultKeyResolver.resolve(exchange)
keyMono
.flatMap { key ->
if (key.isNullOrEmpty()) {
logger.warn("Ключ не разрешен. Блокировка запроса.")
LocalExceptionHandlers.missingKey(exchange)
} else {
logger.info("Разрешенный ключ: $key")
redisRateLimiter.isAllowed(RATE_LIMITER_ID, key)
.flatMap { response ->
if (!response.isAllowed) {
logger.warn("Лимит скорости превышен для ключа: $key")
LocalExceptionHandlers.rateLimitExceeded(exchange)
} else {
logger.info("Лимит скорости разрешен для ключа: $key")
chain.filter(exchange)
}
}
}
}.then()
}
}
override fun newConfig(): Config {
return Config()
}
class Config
}
/**
* Дополнительная конфигурация ограничителя скорости
*/
@Configuration
internal class RedisRateLimiterConfig(
private val authorizationProperties: AuthorizationProperties,
) {
private val logger = LoggerFactory.getLogger(RedisRateLimiterConfig::class.java)
/**
* Ограниченный Redis
*/
@Bean
fun redisRateLimiter(): RedisRateLimiter {
return RedisRateLimiter(1, 1, 1)
}
/**
* Резольвер ключей по умолчанию
*/
@Bean
fun defaultKeyResolver(): KeyResolver {
return KeyResolver { exchange: ServerWebExchange ->
// получаем контекст аутентификации и разрешаем идентификатор провайдера, если он доступен
ReactiveSecurityContextHolder.getContext()
.doOnNext { context -> logger.info("Найден контекст безопасности: ${context.authentication}") }
.mapNotNull { it.authentication as? JwtAuthenticationToken }
.flatMap { authentication ->
val providerId = authentication?.token?.getClaimAsString(authorizationProperties.authProviderSubjectClaim)
if (providerId.isNullOrBlank()) {
logger.warn("Идентификатор провайдера не найден в токене.")
Mono.just("")
} else {
logger.info("Разрешенный идентификатор провайдера для ограничения скорости: $providerId")
Mono.just(providerId)
}
}
.switchIfEmpty(
Mono.defer {
logger.warn("Не найдена аутентификация в контексте безопасности.")
Mono.just("")
}
)
}
}
}
Но ничего не срабатывало, я не мог увидеть никаких логов.
Итак, я добавил это (чтобы все маршруты направлялись на этот же сервер).
Примечание: я не думаю, что это идеальный вариант в окружении балансировщика нагрузки, так как запрос уже достиг сервера ресурсов, и это фактически перенаправляет его обратно на себя.
Но даже с этим я не получаю никаких записей в логах. Я отправляю запрос к эндпоинту hello world, и получаю все 25 ответов очень быстро, менее чем за 1 секунду (когда ограничитель скорости действительно должен заработать после 1 запроса)
Строитель маршрутов
@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("/**") // Соответствие всем путям
.filters { f ->
logger.info("Вход в маршрут ресурса-сервера")
f.filter(rateLimitingFilter.apply(rateLimitingFilter.newConfig()))
}
.uri(serverProperties.resourceServerUri)
}
.build()
}
}
Yaml
Вот моя конфигурация yaml
# настройки по умолчанию для spring
spring:
# настройки spring cloud
cloud:
# настройки spring gateway
gateway:
metrics:
enabled: true
tags:
path:
enabled: true
OAuth Ресурсный сервер
А моя цепочка безопасности ресурсного сервера довольно стандартная:
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity(useAuthorizationManager = true)
internal class ResourceSecurityConfig() {
@Bean
/* цепочка фильтров безопасности для аутентификации и авторизации (реактивно) */
/* это должно быть бессостояние webSession */
fun resourceServerSecurityFilterChain(
http: ServerHttpSecurity,
): SecurityWebFilterChain {
/* включить csrf */
http.csrf { csrf ->
csrf.disable()
}
/* oauth2.0 ресурсный сервер */
http.oauth2ResourceServer { oauth2 ->
oauth2.authenticationManagerResolver(authenticationManagerResolver)
oauth2.authenticationEntryPoint(authenticationEntryPoint)
oauth2.accessDeniedHandler(accessDeniedHandler)
}
/* настроить авторизацию */
http.authorizeExchange { authorize ->
authorize
.pathMatchers(
"/v3/api-docs",
"/swagger-ui.html",
"/webjars/swagger-ui/**",
"/actuator/**").permitAll()
.anyExchange().permitAll()
}
return http.build()
}
Я не уверен, где я ошибся? Возможно, это yaml?
Буду признателен за любую помощь
Ответ или решение
Проблема с неработающим лимитатором запросов в вашем Spring Rest-API может быть обусловлена несколькими факторами. Рассмотрим ключевые элементы вашего кода и конфигурации, чтобы определить возможные причины и решения.
1. Настройка фильтра лимитатора запросов
Ваш фильтр RequestRateLimiterConfig
выглядит реализованным, однако стоит убедиться в том, что он правильно применяется в маршрутах. Код вашего маршрута предполагает, что фильтр корректно применяется к запросам, приходящим на сервер:
fun routeLocator(builder: RouteLocatorBuilder): RouteLocator {
return builder.routes()
.route("resource-server") { r ->
r.path("/**") // Соответствие всем путям
.filters { f ->
logger.info("Entering resource-server route")
f.filter(rateLimitingFilter.apply(rateLimitingFilter.newConfig()))
}
.uri(serverProperties.resourceServerUri)
}
.build()
}
Рекомендация:
Добавьте вывод логов на уровень маршрута, чтобы убедиться, что запросы действительно попадают в фильтр и соответствуют заданным условиям. Возможно, стоит временно установить более детализированное логирование, например, использовать DEBUG
уровень.
2. Проверка конфигурации RedisRateLimiter
Проверьте конфигурацию RedisRateLimiter
:
@Bean
fun redisRateLimiter(): RedisRateLimiter {
return RedisRateLimiter(1, 1, 1) // Позволяет 1 запрос в секунду
}
Вопросы:
- Правильно ли настроено подключение к Redis?
- Работает ли сервер Redis и доступны ли к нему соединения из вашего приложения?
Вы можете протестировать соединение с Redis для подтверждения корректности.
3. Ключ резолвера
Ваш defaultKeyResolver
создает ключи на основе идентификатора пользователя:
return KeyResolver { exchange: ServerWebExchange ->
ReactiveSecurityContextHolder.getContext()
.mapNotNull { it.authentication as? JwtAuthenticationToken }
.flatMap { authentication ->
// Извлечение ID провайдера и его последующая обработка
}
}
Рекомендация:
Убедитесь, что authentication
действительно поступает в резолвер и возвращает значения. Если конфигурация токена некорректна, ключи могут не создаваться, и, как следствие, фильтр не будет работать. Следует добавить отладочные логи внутри defaultKeyResolver
для понимания потока выполнения.
4. Направление запросов
Вы упомянули использование маршрутов для направления запросов обратно в тот же ресурсный сервер. Действительно, такой подход может привести к нежелательному поведению, так как запрос уже достиг сервера. Убедитесь, что вы используете API Gateway правильно, а не направляете запросы не туда. Рассмотрите возможность применения фильтров на уровне API Gateway.
5. Конфигурация YAML
Конфигурация YAML на первый взгляд выглядит простой и не требует изменений. Однако стоит удостовериться, что другие параметры Spring Cloud не конфликтуют с вашим лимитатором. Проверьте полную конфигурацию приложения на наличие несоответствий.
6. Ресурсный сервер
Проверьте конфигурацию вашего ресурсного сервера. Если после разметки запросы проходят, значит проблема может заключаться в их обработке. Убедитесь, что настройки SecurityWebFilterChain
действительно необходимы и корректно работают.
Заключение
Как резюме, выполните следующие шаги:
- Удостоверьтесь, что фильтры и маршрутизация настроены корректно.
- Добавьте логи для проверки потока выполнения и точной конфигурации
keyResolver
. - Проверьте состояние и доступность вашего Redis.
- Избегайте направлений запросов обратно в тот же сервер и корректно тестируйте всю цепочку запросов через ваше приложение.
Если после всех шагов проблема остается, используйте отладку через IDE для более глубокого анализа выполнения кода. Это поможет выделить проблемные места и возобновить работоспособность лимитатора запросов.