You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 00:30:43