OkHttp/Retrofit'te başlık ekleme ve token'ların güvenli güncellenmesi
Kısaca: Global başlıklar (Authorization dahil) için OkHttp Interceptor kullanın; istek bazlı başlıklar için Retrofit anotasyonlarını; 401 durumunda senkron yenileme için Authenticator'ı kullanın; token'ları şifreli depolamada saklayın ve yenileme sırasında yarış durumlarını önleyin.
İçindekiler {{TOC_AUTOMATIC}}
Başlıklar nasıl eklenir
- Interceptor — global başlıklar için (Accept, Authorization). Interceptor, OkHttpClient'a eklenir ve bu istemci tarafından oluşturulan tüm isteklere uygulanır.
AuthInterceptor örneği (Kotlin):
class AuthInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val req = chain.request()
val token = tokenProvider.getAccessToken() // null dönebilir
val newReq = req.newBuilder()
.apply {
header("Accept", "application/json")
if (!token.isNullOrBlank()) header("Authorization", "Bearer $token")
}
.build()
return chain.proceed(newReq)
}
}
İstemci kaydı:
val client = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor(tokenProvider))
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY // prod'da devre dışı bırakın
})
.authenticator(TokenAuthenticator(tokenProvider, authApi))
.build()
- İstek bazlı Retrofit üzerinden:
- Tek bir değer için @Header;
- Statik bir küme için @Headers;
- Çağrı başına dinamik başlıklar için @HeaderMap.
Örnekler:
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 üzerinden manuel olarak istek oluşturduğunuzda (Retrofit kullanırken daha nadir).
Production ortamında Authorization ve refresh token'ları loglamayın. Yayın öncesi loglama ayarlarınızı kontrol edin.
Refresh token ve Authenticator — pratik şablon
Authenticator, OkHttp tarafından 401/407 durumlarında çağrılır ve senkron bir yenileme gerçekleştirerek güncellenmiş başlığa sahip yeni bir Request döndürmeli veya (kullanıcının çıkış yapması gerekiyorsa) null döndürmelidir.
Temel noktalar:
- Authenticator senkron çalışır — yenileme için sync Retrofit çağrısı (execute()) kullanın.
- Yarış durumlarına karşı koruma sağlayın: aynı anda yalnızca bir yenileme işlemi, diğer istekler sonucu bekler (single-flight).
- Döngüleri önlemek için yeniden deneme sayısını sınırlayın.
Senkronizasyonlu basit bir Authenticator örneği:
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 // döngüyü önle
synchronized(this) {
val current = tokenProvider.getAccessToken()
val reqToken = response.request.header("Authorization")?.removePrefix("Bearer ")
if (reqToken != null && reqToken != current) {
// başka bir thread zaten token'ı güncelledi
val newToken = tokenProvider.getAccessToken() ?: return null
return response.request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
}
// senkron yenileme gerçekleştir
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 için ipuçları:
- Authenticator içinde synchronized veya ReentrantLock kullanın.
- Condition/Notify ile bekleme implemente edebilir veya yenilemenin zaten tamamlandığını belirten ayrı bir shared state kullanabilirsiniz.
DI ile entegrasyon:
- Hilt/Dagger aracılığıyla Singleton OkHttpClient ve TokenProvider sağlayın.
- Authenticator tarafından kullanılan AuthApi, eski Authorization'ın eklenmemesi için AuthInterceptor içermeyen bir Retrofit istemcisi olmalıdır.
Token depolama ve güvenlik
- Access token'ı kısa süreli saklayın; refresh token'ı daha uzun süreli ancak şifreli depolamada tutun.
- EncryptedSharedPreferences veya DataStore + Android Keystore / MasterKey anahtarlarını kullanın.
- Çıkış yaparken her iki token'ı ve tüm önbellekleri silin.
- Prod ortamında başlıkların ve gizli bilgiler içeren gövdelerin loglanmasını devre dışı bırakın.
TokenProvider basit bir arayüz olmalıdır: getAccessToken(), getRefreshToken(), saveTokens(), clearTokens(). Bu, test etmeyi ve implementasyonu değiştirmeyi kolaylaştırır.
Sık yapılan hatalar
- Senkronize edilmemiş yenileme → birçok paralel yenileme isteği.
- OkHttpClient'da Authenticator eksikliği → 401 otomatik olarak işlenmez.
- Prod ortamında gizli bilgilerin loglanması.
- Başarısız yenilemede sonsuz retry döngüleri (deneme sayısını saymama).
SSS
- Authenticator'da coroutine kullanılabilir mi? Hayır: Authenticator senkron olarak çağrılır, yenileme için execute() kullanın. Coroutine mantığını uygulamanın üst katmanlarında tutun.
- Belirli hostlar için nasıl başlık gönderilir? Interceptor'da request.url.host'u kontrol edin ve başlığı yalnızca gerekli hostlar için ekleyin.
- Yenileme senaryoları nasıl test edilir? MockWebServer kullanın: 401'i simüle edin, yenileme yanıtını verin ve tekrarlanan isteği kontrol edin.
Özet: Global başlıklar için Interceptor, 401 durumunda güvenli senkron yenileme için Authenticator, istek bazlı başlıklar için Retrofit anotasyonları kullanın; token'ları şifreli depolamada saklayın ve yenileme sırasında yarış durumlarına karşı koruma sağlayın.