Ktor中Dagger Hilt注入Bearer Token首次请求失效优化
问题场景与优化需求
当前使用Ktor发起请求时,多数情况下首次请求会失败,随后调用refreshTokens获取Token并重试,导致大量未认证请求,多数请求执行两次,网络调用量近乎翻倍。只有之前调用过相同端点时,请求才能正常完成认证。
需求:实现Token一次性提供给所有Ktor端点,仅在有效登录时更新Token,使用androidx.datastore:datastore-preferences存储Token,settingsRepository.getAuthToken()可正确获取Token。
现有代码实现
依赖注入(DI)实现
@Provides @Singleton fun provideBearerAuthProvider( settingsRepository: SettingsRepository ) = BearerAuthProvider( realm = null, loadTokens = { val token = settingsRepository.getAuthToken() if (token == null) { BearerTokens(accessToken = "fake", refreshToken = "") } else { BearerTokens(accessToken = token, refreshToken = "") } }, refreshTokens = { val token = settingsRepository.requireAuthToken() token?.let { BearerTokens(accessToken = it, refreshToken = "") } }, sendWithoutRequestCallback = { httpRequestBuilder -> httpRequestBuilder.url.host == USER_LOGIN } ).also { bearerAuthProvider -> settingsRepository.addOnClearListener(bearerAuthProvider::clearToken) } @Provides @Singleton fun provideApiClient( bearerAuthProvider: BearerAuthProvider, ) = HttpClient(Android) { install(Logging) { logger = Logger.ANDROID LogLevel.ALL } install(ContentNegotiation) { gson() } install(Auth) { providers += bearerAuthProvider } }
ApiService基类实现
abstract class BaseApiService( protected val httpClient: HttpClient, protected val baseUrl: String, ) { protected suspend inline fun <reified T> get(endpoint: String, block: HttpRequestBuilder.() -> Unit = {}): T { return httpClient.get(urlString = baseUrl + endpoint, block = block).body() } protected suspend inline fun <reified T> post(endpoint: String, block: HttpRequestBuilder.() -> Unit): T { return httpClient.post(urlString = baseUrl + endpoint, block = block).body() } protected suspend inline fun <reified T> put(endpoint: String, block: HttpRequestBuilder.() -> Unit = {}): T { return httpClient.put(urlString = baseUrl + endpoint, block = block).body() } protected suspend inline fun <reified T> patch(endpoint: String, block: HttpRequestBuilder.() -> Unit): T { return httpClient.patch(urlString = baseUrl + endpoint, block = block).body() } protected suspend inline fun <reified T> delete(endpoint: String, block: HttpRequestBuilder.() -> Unit = {}): T { return httpClient.delete(urlString = baseUrl + endpoint, block = block).body() } }
UserApiService实现
class UserApiService @Inject constructor( httpClient: HttpClient ) : BaseApiService(httpClient, BASE_URL) { suspend fun authenticate(): BasicApiResponse<Unit> = get(endpoint = USER_AUTHENTICATE) suspend fun login(loginRequest: LoginRequest): BasicApiResponse<AuthApiResponse> = post(endpoint = USER_LOGIN) { contentType(ContentType.Application.Json) setBody(loginRequest) } suspend fun updateUserProfile(updateProfileRequest: UpdateProfileRequest): BasicApiResponse<UserApiResponse> = patch(endpoint = USER) { contentType(ContentType.Application.Json) setBody(updateProfileRequest) } }
优化方案
1. 修正Token加载逻辑,避免无效请求
当前loadTokens在Token为空时返回"fake"无效Token,导致首次请求被拒绝触发重试。直接返回null让Auth插件识别无有效Token,跳过无效请求携带:
@Provides @Singleton fun provideBearerAuthProvider( settingsRepository: SettingsRepository ) = BearerAuthProvider( realm = null, loadTokens = { // Token为空时返回null,不携带无效Token settingsRepository.getAuthToken()?.let { BearerTokens(accessToken = it, refreshToken = "") } }, refreshTokens = { oldTokens -> // 替换为真正的后端刷新逻辑,而非本地取Token val refreshResponse = apiClient.post<AuthApiResponse>(REFRESH_TOKEN_ENDPOINT) { headers { append(HttpHeaders.Authorization, "Bearer ${oldTokens.refreshToken}") } }.body() settingsRepository.saveAuthToken(refreshResponse.accessToken) BearerTokens(accessToken = refreshResponse.accessToken, refreshToken = refreshResponse.refreshToken) }, sendWithoutRequestCallback = { httpRequestBuilder -> // 修正判断逻辑:匹配端点路径而非host httpRequestBuilder.url.encodedPath == USER_LOGIN } ).also { bearerAuthProvider -> settingsRepository.addOnClearListener(bearerAuthProvider::clearToken) }
2. 监听Token变化,主动更新Auth插件缓存
登录成功后Token更新,但Auth插件不会自动感知,需添加监听主动更新:
// 扩展BearerAuthProvider,添加更新Token的方法 fun BearerAuthProvider.updateToken(newToken: String?) { this.tokenStorage = newToken?.let { BearerTokens(accessToken = it, refreshToken = "") } } // 在DI中添加Token变化监听 @Provides @Singleton fun provideBearerAuthProvider( settingsRepository: SettingsRepository ) = BearerAuthProvider( // 保持其他配置不变 ).also { bearerAuthProvider -> settingsRepository.addOnClearListener(bearerAuthProvider::clearToken) // 监听DataStore的Token变化,实时更新Auth插件 CoroutineScope(Dispatchers.IO).launch { settingsRepository.tokenChangeFlow.collect { newToken -> bearerAuthProvider.updateToken(newToken) } } }
同时在SettingsRepository中添加Token变化流:
class SettingsRepository(private val dataStore: DataStore<Preferences>) { private val AUTH_TOKEN_KEY = stringPreferencesKey("auth_token") private val _tokenChangeFlow = MutableSharedFlow<String?>() val tokenChangeFlow: SharedFlow<String?> = _tokenChangeFlow suspend fun getAuthToken(): String? { return dataStore.data.first()[AUTH_TOKEN_KEY] } suspend fun saveAuthToken(token: String) { dataStore.edit { preferences -> preferences[AUTH_TOKEN_KEY] = token } // 发送Token变化事件 _tokenChangeFlow.emit(token) } // 其他方法... }
3. 登录成功后立即同步Token
在UserApiService的登录方法中,成功后直接保存Token触发更新:
suspend fun login(loginRequest: LoginRequest): BasicApiResponse<AuthApiResponse> { val response = post(endpoint = USER_LOGIN) { contentType(ContentType.Application.Json) setBody(loginRequest) } // 登录成功后立即保存Token,触发Auth插件更新 if (response.isSuccess) { response.data?.accessToken?.let { settingsRepository.saveAuthToken(it) } } return response }
4. 添加刷新锁,避免重复请求
防止同一时间多个请求触发重复Token刷新,添加互斥锁:
private val refreshLock = Mutex() refreshTokens = { oldTokens -> refreshLock.withLock { // 先检查本地Token是否已更新,避免重复刷新 val currentToken = settingsRepository.getAuthToken() if (currentToken != null && currentToken != oldTokens.accessToken) { return@withLock BearerTokens(accessToken = currentToken, refreshToken = oldTokens.refreshToken) } // 调用后端刷新接口 val refreshResponse = apiClient.post<AuthApiResponse>(REFRESH_TOKEN_ENDPOINT) { headers { append(HttpHeaders.Authorization, "Bearer ${oldTokens.refreshToken}") } }.body() settingsRepository.saveAuthToken(refreshResponse.accessToken) BearerTokens(accessToken = refreshResponse.accessToken, refreshToken = refreshResponse.refreshToken) } }
内容的提问来源于stack exchange,提问作者CryptoCode
相关产品推荐
相关产品推荐

