使用Retrofit/OkHttp的Authenticator时如何防止并行刷新令牌请求?

问题描述 投票:0回答:1

我偶然发现一个问题,我开始向我构建的后端服务器发送过于并行的刷新令牌请求,这导致了并发问题,其中存在竞争条件,所有这些并行请求同时请求和更新不同的刷新令牌时间。

我想到的唯一解决方案是使用 StateFlow、Channel 和无作用域 IO 协程来观察刷新状态,以便只有第一个刷新令牌请求成功,并且在刷新时,其他并行请求将被阻止观察,直到它们从第一个刷新令牌请求获取信号以使用新令牌。

它有效,但我对 Kotlin 及其协程 API 很陌生,而且它看起来很老套,我忍不住,但认为肯定有一种更明智的方法来解决这个问题。

class MyAuthenticator @Inject constructor(
    private val refreshTokenUseCase: RefreshTokenUseCase,
    private val sharedPrefs: SharedPreferences
) : Authenticator {

    private val isRefreshingToken = MutableStateFlow(false)
    private val newRequest = Channel<Request>()

    override fun authenticate(route: Route?, response: Response): Request? {

        // logic to handle blocking parallel refresh token requests to wait for the first refresh token request to use it instead of useless api calls:
        if (isRefreshingToken.value) {
            CoroutineScope(Dispatchers.IO).launch {
                isRefreshingToken.collect { isRefreshingToken ->
                    if (!isRefreshingToken) {
                        val newToken = sharedPrefs.getToken().orEmpty()
                        val req = response.request.newBuilder()
                            .header("Authorization", "Bearer $newToken")
                            .build()
                        newRequest.send(req)
                    }
                }
            }
            return runBlocking(Dispatchers.IO) {
                newRequest.receive()
            }
        }

        isRefreshingToken.value = true

        // logic to handle refreshing the token
        runBlocking(Dispatchers.IO) {
            refreshTokenUseCase() // internally calls refresh token api then saves the token to shared prefs
        }.let { result ->
            isRefreshingToken.value = false
            return if (result.isSuccess) {
                val newToken = sharedPrefs.getToken().orEmpty()
                response.request.newBuilder()
                    .header("Authorization", "Bearer $newToken")
                    .build()
            } else {
                // logic to handle failure (logout, etc)
                null
            }
        }

    }
}

我搜索了整个堆栈溢出,虽然我找到了许多建议的解决方案,但没有一个真正起作用,其中一半建议使用同步来强制并行以有序的方式启动,这仍然浪费地调用 API 来获取刷新令牌太多次了。

android retrofit okhttp refresh-token
1个回答
1
投票

最终将authenticate()方法块与@Synchronized同步,同时还检查请求的标头令牌是否与本地持久令牌不同,以了解它是否已刷新。奇迹般有效。只需确保刷新令牌 api 调用在后台线程上阻塞(例如 runBlocking(Dispatchers.IO)),并在更新共享首选项中的访问令牌时使用 .commit() 而不是 .async()。

class MyAuthenticator @Inject constructor(
    private val refreshTokenUseCase: RefreshTokenUseCase,
    private val sharedPrefs: SharedPreferences
) : Authenticator {

    @Synchronized // annotate with @Synchronized to force parallel threads/coroutines to block and wait in an ordered manner when accessing authenticate()
    override fun authenticate(route: Route?, response: Response): Request? {

    // prevent parallel refresh requests
        val accessToken = sharedPrefs.getToken()
        val alreadyRefreshed = response.request.header("Authorization")?.contains(accessToken, true) == false
        if (alreadyRefreshed) { // if request's header's token is different, then that means the access token has already been refreshed and we return the response with the locally persisted token in the header 
        return response.request.newBuilder()
        .header("Authorization", "Bearer $accessToken")
        .build()
        }

        // logic to handle refreshing the token
        runBlocking(Dispatchers.IO) {
            refreshTokenUseCase() // internally calls refresh token api then saves the token to shared prefs synchronously
        }.let { result ->
            return if (result.isSuccess) {
                val newToken = sharedPrefs.getToken().orEmpty()
                response.request.newBuilder()
                    .header("Authorization", "Bearer $newToken")
                    .build()
            } else {
                // logic to handle failure (logout, etc)
                null
            }
        }

    }
}
© www.soinside.com 2019 - 2024. All rights reserved.