我偶然发现一个问题,我开始向我构建的后端服务器发送过于并行的刷新令牌请求,这导致了并发问题,其中存在竞争条件,所有这些并行请求同时请求和更新不同的刷新令牌时间。
我想到的唯一解决方案是使用 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 来获取刷新令牌太多次了。
最终将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
}
}
}
}