В статье я хочу показать, каким образом можно реализовать отправку запросов с авторизацией при использовании Spring Cloud OpenFeign клиента для работы с АПИ.

Что такое OpenFeign и зачем он нужен

Spring Cloud OpenFeign - это реализация feign клиента для работы со Spring Boot. Feign - это декларативный HTTP клиент, для упрощения написания интеграций с различными АПИ. Подробнее можно почитать в официальной документации и посмотреть код на гитхаб .

Можно выделить следующие плюсы использования OpenFeign:

  • не нужно заморачиваться созданием и управлением конкретной реализацией HTTP клиента (клиент создается автоматически, а различные настройки, при необходимости, можно проводить через application.yml)

  • не нужно как-то настраивать обработку ошибок в ответе на ваш запрос, OpenFeign сам преобразовывает все не 2ХХ и не 3ХХ ответы в FeignException

  • если вы используете метрики (а если и не используете) - OpenFeign из коробки настраивает набор Micrometer метрик.

Добавление токенов авторизации в запрос

При необходимости отправить запрос с указанием данных авторизации OpenFeign позволяет нам идти несколькими путями.

Первый и самый простой - передать токен как параметр метода отправки запроса.

@FeignClient(value = "example-client", url= "\${external.url}")
interface ExampleClient {
    @GetMapping("/info")
    fun getInfo(@RequestHeader("Authorization") bearerToken: String): String
}

class Demo(
    private val exampleClient: ExampleClient
) {
    val testResult = exampleClient.getInfo("Bearer customBearerToken")
}

Но в этом случае вся ответственность за получение и обновление токена ложится на плечи вызывающего сервиса, что не очень хорошо. Нам может помочь еще одна фишка OpenFeign, а именно использование классов конфигурации клиента.

Ниже рассмотрим два примера конфигурации клиента - для простой OAuth2 client_credentials авторизации с использование Spring Security и чуть более сложной, кастомной авторизации.

OAuth2 авторизация

В этом случае почти всю работу за нас делает магия Spring (а точнее Spring Security).

Сначала нужно настроить Spring Security для доступа к некоторому АПИ (в application.yml). Необходимо указать clienId, clientSecret и адрес для получения токена внешнего сервиса.

spring:
  security:
    oauth2:
      client:
        registration:
          external:
            client-id: "client_id"
            client-secret: "client_secret"
            authorization-grant-type: client_credentials
        provider:
          external:
            token-uri: https://external.service/oauth/access_token

Затем нужно сконфигурировать добавление токенов при отправке запросов через OpenFeign клиент

@FeignClient(value = "example-client", url= "\${external.url}", 
             configuration = [FeignClientConfiguration::class])
interface ExampleClient {
    @GetMapping("/info")
    fun getInfo()
}

Обратите внимание на параметр configuration в аннотации @FeignClient . Он указывает на то, какой класс будет использоваться для конфигурации клиента, реализующего этот интерфейс.

Посмотрим подробнее на класс конфигурации (подробно про возможности конфигурирования есть в документации). Обратите внимание, что мы не аннотируем этот класс @Configuration, если не хотим использовать бины из этого файла по умолчанию во всем проекте.

class FeignClientConfiguration(
 private val oAuth2AuthorizedClientManager: OAuth2AuthorizedClientManager
) {
    @Bean
    fun requestInterceptor(): RequestInterceptor = RequestInterceptor { template ->
        val accessToken = getAccessToken()
        template.header("Authorization", "Bearer ${accessToken?.tokenValue}")
    }

    private fun getAccessToken(): OAuth2AccessToken? {
        val request = OAuth2AuthorizeRequest
            .withClientRegistrationId("external")
            .principal("principal-name")
            .build()
        return oAuth2AuthorizedClientManager.authorize(request)?.accessToken
    }
}

В этом классе переопределяется bean типа feign.RequestInterceptor. Этот bean перехватывает запрос перед отправкой и дописывает заголовок Authorization. Токен при этом будет получен через экземпляр OAuth2AuthorizedClientManager - стандартную реализацию менеджера авторизации OAuth2 из состава Spring Security.

На этом всё, теперь каждый запрос перед отправкой будет обогащаться нужным заголовком, а за получение и обновление токена будет отвечать Spring.

Кастомная авторизация

Рассмотрим случай, когда Spring не предоставляет готовых решений по управлению токенами авторизации. Последовательность действий здесь будет та же самая, но менеджер авторизации придется реализовать самостоятельно.

Сам интерфейс OpenFeign клиента остается таким же, как в предыдущем разделе. Изменения будут только в классе конфигурации.

class FeignClientConfiguration(
    private val customAuthorizationManager: CustomAuthorizationManager
) {
    @Bean
    fun requestInterceptor(): RequestInterceptor = RequestInterceptor { template ->
        if (!template.headers().containsKey("Authorization")) {
            val accessToken = customAuthorizationManager.getAccessToken()
            template.header("Authorization", "Bearer $accessToken")
        }
    }
}

Можно заметить, что вместо стандартного OAuth2AuthorizedClientManager появляется некий CustomAuthorizationManager . Квази-реализация такого менеджера приведена ниже

@Component
class CustomAuthorizationManager(
    private val tokenManager: TokenManager,
    private val tokenRepository: TokenRepository
) {
    fun getAccessToken(): String {
        val userName = SecurityContextHolder.getContext().authentication.name
        val token = tokenRepository.getTokenByUserName(userName)

        return if (tokenManager.checkIfExpired(token)) {
            tokenManager.refreshToken(token)
        } else { 
          token 
        }
    }
}

В примере TokenManager - это некий класс, который умеет проверять, не истекло ли время жизни токена (например через сумму created_at и expired_at токена или через отправку тестового запроса в сервис и разбора ответа) а также умеет отправлять запрос на обновление токена. TokenRepository - это репозиторий для поиска токена в нашем внутреннем хранилище.

Для получения токена нужна узнать, от чьего имени будем отправлять запрос. В примере используется Security Context приложения для определения текущего пользователя, а затем происходит поиск ассоциированных с пользователем токенов в хранилище токенов. И, при необходимости, происходит обновление токена.

Текущую схему можно оптимизировать. В примере, при отправке каждого запроса приходится ходить в хранилище за токеном. А если происходит несколько запросов от имени одного и того же пользователя приложения, то и запросов в хранилище будет несколько. Если же организовать некий кэш токенов или хранить токен в собственной реализации org.springframework.security.core.Authentication, то количество запросов к хранилищу можно существенно уменьшить.

Комментарии (0)