Добавление заголовков и безопасное обновление токенов в OkHttp/Retrofit

Коротко: используйте OkHttp Interceptor для глобальных заголовков (включая Authorization), аннотации Retrofit для per‑request заголовков, а Authenticator для sync‑refresh при 401; храните токены в зашифрованном хранилище и предотвращайте гонки при обновлении.

Оглавление {{TOC_AUTOMATIC}}

Как добавлять заголовки

  1. Interceptor — для глобальных заголовков (Accept, Authorization). Интерсептор добавляется в OkHttpClient и применяется ко всем запросам, созданным этим клиентом.

Пример AuthInterceptor (Kotlin):

class AuthInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val req = chain.request()
        val token = tokenProvider.getAccessToken() // может вернуть null
        val newReq = req.newBuilder()
            .apply {
                header("Accept", "application/json")
                if (!token.isNullOrBlank()) header("Authorization", "Bearer $token")
            }
            .build()
        return chain.proceed(newReq)
    }
}

Регистрация клиента:

val client = OkHttpClient.Builder()
    .addInterceptor(AuthInterceptor(tokenProvider))
    .addInterceptor(HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY // отключать в prod
    })
    .authenticator(TokenAuthenticator(tokenProvider, authApi))
    .build()
  1. Per‑request через Retrofit:
  • @Header для одного значения;
  • @Headers для статического набора;
  • @HeaderMap для динамических заголовков на вызов.

Примеры:

interface Api {
    @GET("items")
    suspend fun getItems(@Header("X-Client") client: String): List<Item>

    @GET("profile")
    @Headers("Accept: application/json")
    suspend fun profile(@HeaderMap headers: Map<String, String>): Profile
}
  1. Request.Builder — когда создаёте запрос вручную через OkHttp (реже при использовании Retrofit).

Нельзя логировать Authorization и refresh token в production. Проверьте настройки логирования перед релизом.

Refresh token и Authenticator — практический шаблон

Authenticator вызывается OkHttp при 401/407 и должен выполнить синхронный refresh и вернуть новый Request с обновлённым заголовком или null (если нужно разлогинить пользователя).

Ключевые моменты:

  • Authenticator работает синхронно — используйте sync Retrofit вызов (execute()).
  • Защищайте от гонок: только один refresh одновременно, остальные запросы ждут результата (single‑flight).
  • Ограничьте повторные попытки (чтобы избежать loop).

Пример простого Authenticator с синхронизацией:

class TokenAuthenticator(
    private val tokenProvider: TokenProvider,
    private val authApi: AuthApi
) : Authenticator {

    @Volatile private var refreshing = false

    override fun authenticate(route: Route?, response: Response): Request? {
        if (responseCount(response) >= 2) return null // предотвратить loop

        synchronized(this) {
            val current = tokenProvider.getAccessToken()
            val reqToken = response.request.header("Authorization")?.removePrefix("Bearer ") 
            if (reqToken != null && reqToken != current) {
                // другой поток уже обновил токен
                val newToken = tokenProvider.getAccessToken() ?: return null
                return response.request.newBuilder()
                    .header("Authorization", "Bearer $newToken")
                    .build()
            }

            // выполняем синхронный refresh
            val refresh = tokenProvider.getRefreshToken() ?: return null
            val call = authApi.refreshTokenSync(RefreshRequest(refresh)) // sync call
            val resp = call.execute()
            if (!resp.isSuccessful) {
                tokenProvider.clearTokens()
                return null
            }
            val newTokens = resp.body() ?: return null
            tokenProvider.saveTokens(newTokens)
            val newAccess = newTokens.accessToken ?: return null
            return response.request.newBuilder()
                .header("Authorization", "Bearer $newAccess")
                .build()
        }
    }

    private fun responseCount(response: Response): Int {
        var res = response.priorResponse
        var result = 1
        while (res != null) {
            result++; res = res.priorResponse
        }
        return result
    }
}

Советы по single‑flight:

  • Используйте synchronized или ReentrantLock в Authenticator.
  • Можно реализовать ожидание с Condition/Notify или использовать отдельный shared state, который указывает, что refresh уже выполнен.

Интеграция с DI:

  • Предоставьте Singleton OkHttpClient и TokenProvider через Hilt/Dagger.
  • AuthApi, используемый Authenticator'ом, должен быть Retrofit-клиентом без AuthInterceptor, чтобы refresh запрос не добавлял старый Authorization.

Хранение токенов и безопасность

  • Храните access token короткое время; refresh token — дольше, но в зашифрованном хранилище.
  • Используйте EncryptedSharedPreferences или DataStore + ключи в Android Keystore / MasterKey.
  • При logout удаляйте оба токена и все кэши.
  • В проде отключайте логирование заголовков и тел с секретами.

TokenProvider должен быть простым интерфейсом: getAccessToken(), getRefreshToken(), saveTokens(), clearTokens(). Это упрощает тестирование и замену реализации.

Частые ошибки

  • Несинхронизированный refresh → множество параллельных запросов refresh.
  • Отсутствует Authenticator в OkHttpClient → 401 не обрабатывается автоматически.
  • Логирование секретов в prod.
  • Бесконечные retry‑цыклы при неудачном refresh (не считать попытки).

FAQ

  • Можно ли использовать корутины в Authenticator? Нет: Authenticator вызывается синхронно, используйте execute() для refresh. Логику с корутинами держите в верхних слоях приложения.
  • Как отправлять заголовок только для определённых хостов? В Interceptor проверяйте request.url.host и добавляйте заголовок только для нужных хостов.
  • Как тестировать refresh‑сценарии? Используйте MockWebServer: эмулируйте 401, ответ refresh и проверяйте повторный запрос.

Резюме: Interceptor для глобальных заголовков, Authenticator — для безопасного sync refresh при 401, per‑request заголовки — через аннотации Retrofit; храните токены в зашифрованном хранилище и защищайте от гонок при refresh.