Авторизация HTTP-клиента

Провайдер авторизации добавляет application-interceptor OkHttp и, при необходимости, Authenticator. Режим выбирается ключом:

api.<key>.auth.type=<режим>

Встроенные режимы

Режим Обязательные параметры Поведение

NONE

нет

Запрос передаётся без изменений.

BASIC

auth.username, auth.password

В auth.header записывается значение HTTP Basic, сформированное через okhttp3.Credentials.basic.

STATIC_TOKEN

auth.token

В auth.header записывается токен без добавления Bearer, JWT или другого префикса.

EXTERNAL_TOKEN

@TokenProvider на интерфейсе

Токен читается из хранилища. После ответа, для которого OkHttp вызвал Authenticator, токен генерируется и запрос повторяется.

CUSTOM

@AuthenticationProvider на интерфейсе

Используется собственная реализация interceptor/authenticator.

Общие параметры:

Ключ По умолчанию Назначение

api.<key>.auth.header

Authorization

Имя заголовка для BASIC, STATIC_TOKEN, EXTERNAL_TOKEN и некоторых собственных провайдеров.

api.<key>.auth.username

нет

Имя пользователя для BASIC.

api.<key>.auth.password

нет

Пароль для BASIC.

api.<key>.auth.token

нет

Статический токен; также доступен собственным провайдерам.

api.<key>.auth.secretKey

нет

Дополнительный секрет для собственных провайдеров.

api.<key>.auth.tokenType

JWT

Префикс внешнего токена: JWT или BEARER. Регистр сохраняется именно таким.

api.<key>.auth.tokenRepositoryProvider

LOCAL

Хранилище внешнего токена: LOCAL или REDIS.

api.<key>.auth.tokenExpireTime

86400

Срок жизни внешнего токена в хранилище, секунды.

Значения логинов, паролей и токенов не следует помещать в исходный код, примеры документации или логи.

Запрос без авторизации

@NoAuth отключает встроенную или собственную авторизацию для отдельного метода. Это нужно, например, когда TokenProvider получает токен через тот же API.

import retrofit2.http.Body
import retrofit2.http.POST
import ru.bgcrm.util.api.tag.NoAuth

interface CatalogApi {
    @NoAuth
    @POST("api/auth")
    fun authenticate(@Body credentials: Credentials): AccessToken
}

TaggingInterceptor переносит @NoAuth в тег OkHttp-запроса. Все встроенные провайдеры, а также MasterTokenAuthenticationProvider и XSecretAuthenticationProvider, проверяют этот тег.

BASIC

api.catalog.auth.type=BASIC
api.catalog.auth.header=Authorization
api.catalog.auth.username=<service-user>
api.catalog.auth.password=<service-password>

Параметры проверяются при выполнении запроса, а не при создании клиента. Поэтому клиент с неполной конфигурацией может успешно создаться и завершиться BGException только на первом защищённом вызове.

STATIC_TOKEN

api.catalog.auth.type=STATIC_TOKEN
api.catalog.auth.header=Authorization
api.catalog.auth.token=Bearer <service-token>

Провайдер передаёт auth.token как есть. Если сервис ожидает Bearer, префикс нужно включить в само значение настройки.

EXTERNAL_TOKEN

Интерфейс API связывается с генератором аннотацией @TokenProvider:

import ru.bgcrm.util.api.annotation.Api
import ru.bgcrm.util.api.annotation.TokenProvider

@Api(name = "catalog")
@TokenProvider(providerClass = CatalogTokenProvider::class)
interface CatalogApi

Генератор наследует ru.bgcrm.util.api.auth.provider.token.TokenProvider и обязан иметь открытый конструктор (Class<*>, ApiClientsConfig.ApiConfig):

class CatalogTokenProvider(
    apiClass: Class<*>,
    apiConfig: ApiClientsConfig.ApiConfig
) : TokenProvider(apiClass, apiConfig) {
    override fun generateToken(): String {
        // Получить токен у внешнего сервиса.
        return requestToken()
    }
}
api.catalog.auth.type=EXTERNAL_TOKEN
api.catalog.auth.header=Authorization
api.catalog.auth.tokenType=BEARER
api.catalog.auth.tokenRepositoryProvider=REDIS
api.catalog.auth.tokenExpireTime=3600

Порядок работы:

  1. Перед запросом провайдер ищет токен по ключу api:<Api.name>:token.

  2. Найденное значение отправляется как <tokenType> <token>.

  3. Если OkHttp вызывает Authenticator после неуспешной аутентификации, вызывается generateToken().

  4. Новый токен сохраняется и запрос повторяется.

  5. После двух повторов с добавленным заголовком дальнейшая аутентификация прекращается.

LOCAL хранит токен в памяти текущей JVM. После перезапуска он исчезает, а в кластере у каждого экземпляра ERP будет свой токен. REDIS делает токен общим для экземпляров. Неположительный tokenExpireTime для LOCAL означает хранение без срока, а для Redis передаётся в команду EXPIRE и не имеет такой семантики.

CUSTOM

Собственный провайдер наследует ru.bgcrm.util.api.auth.provider.AuthenticationProvider, реализует interceptor и authenticator и имеет открытый конструктор (Class<*>, ApiClientsConfig.ApiConfig).

class CatalogAuthenticationProvider(
    clazz: Class<*>,
    apiConfig: ApiClientsConfig.ApiConfig
) : AuthenticationProvider(clazz, apiConfig) {
    override fun createInterceptor() = Interceptor { chain ->
        val request = chain.request()
        if (request.tag(NoAuth::class.java) != null) {
            return@Interceptor chain.proceed(request)
        }

        val token = requireNotNull(apiConfig.authConfig.token)
        chain.proceed(
            request.newBuilder()
                .header("Api-Key", token)
                .build()
        )
    }

    override fun createAuthenticator() = createDefaultAuthenticator()
}

@Api(name = "catalog")
@AuthenticationProvider(providerClass = CatalogAuthenticationProvider::class)
interface CatalogApi
api.catalog.auth.type=CUSTOM
api.catalog.auth.token=<service-token>

В ядре есть две готовые реализации для режима CUSTOM:

  • MasterTokenAuthenticationProvider отправляет auth.token в фиксированном заголовке master-token;

  • XSecretAuthenticationProvider отправляет auth.token в auth.header, а auth.secretKey — в X-Secret.

Они не имеют отдельных значений auth.type: интерфейс должен явно ссылаться на нужный класс через @AuthenticationProvider, а настройка должна содержать auth.type=CUSTOM.