Добавление заголовков и безопасное обновление токенов в OkHttp/Retrofit
Коротко: используйте OkHttp Interceptor для глобальных заголовков (включая Authorization), аннотации Retrofit для per‑request заголовков, а Authenticator для sync‑refresh при 401; храните токены в зашифрованном хранилище и предотвращайте гонки при обновлении.
Оглавление {{TOC_AUTOMATIC}}
Как добавлять заголовки
- 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()
- 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
}
- 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.