Token refreshing with Retrofit and Interceptor for multiple requests in Android

huangapple go评论66阅读模式
英文:

Token refreshing with Retrofit and Interceptor for multiple requests in Android

问题

I understand you'd like a translation of the code portion you provided. Here's the translated code:

class AuthInterceptor(private val authorizationRepository: AuthorizationRepository) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {

        val originalRequest = chain.request()

        val userIsNotAuthorize = runBlocking { !authorizationRepository.userIsAuthorize().first() }

        if (userIsNotAuthorize) {
            return chain.proceed(originalRequest)
        }

        val accessToken = authorizationRepository.getAccessToken()

        val tokenHasExpired = authorizationRepository.tokenHasExpired()

        return if (tokenHasExpired) {
            chain.proceedDeletingTokenOnError(
                originalRequest.newBuilder()
                    .addHeaders(accessToken)
                    .build()
            )
        } else {
            val refreshTokenRequest = originalRequest.newBuilder()
                .post("".toRequestBody("application/json".toMediaType()))
                .url(BuildConfig.BASE_URL + "/v1/user/refresh")
                .addHeaders(accessToken)
                .build()

            val refreshResponse = chain.proceedDeletingTokenOnError(refreshTokenRequest)

            refreshResponse.use {
                if (it.isSuccessful) {
                    val refreshedToken = Gson().fromJson(
                        it.body?.string(),
                        AuthorizedUserDto::class.java
                    )
                    authorizationRepository.setAccessToken(
                        refreshedToken.items.token,
                        refreshedToken.items.expiredAt
                    )
                    val newCall = originalRequest.newBuilder()
                        .addHeaders(refreshedToken.items.token)
                        .build()
                    chain.proceedDeletingTokenOnError(newCall)
                } else {
                    chain.proceedDeletingTokenOnError(originalRequest)
                }
            }
        }
    }

    private inline fun Request.Builder.addHeaders(token: String) =
        this.apply { header("Authorization", "Bearer $token") }

    private fun Interceptor.Chain.proceedDeletingTokenOnError(request: Request): Response {
        val response = proceed(request)
        if (response.code == 401) {
            runBlocking { authorizationRepository.changeUserState(UserState.NotAuthorized) }
        }
        return response
    }
}

Please note that code translations might not capture all the context and dependencies, so it's important to review and adapt the code as needed for your specific use case.

英文:

How to handle token refresh for multiple requests in Interceptor?

I'm implementing token refresh functionality in my Android app using an AuthInterceptor in Retrofit. The code provided works well for a single request, but I'm encountering issues when multiple requests are made simultaneously.

Here's the relevant code snippet:

class AuthInterceptor(private val authorizationRepository: AuthorizationRepository) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {

        val originalRequest = chain.request()

        val userIsNotAuthorize = runBlocking { !authorizationRepository.userIsAuthorize().first() }

        if (userIsNotAuthorize) {
            return chain.proceed(originalRequest)
        }

        val accessToken = authorizationRepository.getAccessToken()

        val tokenHasExpired = authorizationRepository.tokenHasExpired()

        return if (tokenHasExpired) {
            chain.proceedDeletingTokenOnError(
                originalRequest.newBuilder()
                    .addHeaders(accessToken)
                    .build()
            )
        } else {
            val refreshTokenRequest = originalRequest.newBuilder()
                .post("".toRequestBody("application/json".toMediaType()))
                .url(BuildConfig.BASE_URL + "/v1/user/refresh")
                .addHeaders(accessToken)
                .build()

            val refreshResponse = chain.proceedDeletingTokenOnError(refreshTokenRequest)

            refreshResponse.use {
                if (it.isSuccessful) {
                    val refreshedToken = Gson().fromJson(
                        it.body?.string(),
                        AuthorizedUserDto::class.java
                    )
                    authorizationRepository.setAccessToken(
                        refreshedToken.items.token,
                        refreshedToken.items.expiredAt
                    )
                    val newCall = originalRequest.newBuilder()
                        .addHeaders(refreshedToken.items.token)
                        .build()
                    chain.proceedDeletingTokenOnError(newCall)
                } else {
                    chain.proceedDeletingTokenOnError(originalRequest)
                }
            }
        }
    }

    private inline fun Request.Builder.addHeaders(token: String) =
        this.apply { header("Authorization", "Bearer $token") }

    private fun Interceptor.Chain.proceedDeletingTokenOnError(request: Request): Response {
        val response = proceed(request)
        if (response.code == 401) {
            runBlocking { authorizationRepository.changeUserState(UserState.NotAuthorized) }
        }
        return response
    }
}

The issue I'm facing is that when multiple requests are made and the token has expired, the interceptor proceeds with token refresh for each request individually. As a result, the refresh token endpoint may be called multiple times, causing the token to become invalid.

I've tried debugging my app and noticed that the /v1/user/refresh request can be triggered multiple times. I'm wondering if there is a better solution to handle token refresh for multiple requests simultaneously. Any suggestions or alternative approaches would be greatly appreciated. Thank you!

答案1

得分: 1

以下是使用 ReentrantLock 的拦截器代码部分的翻译:

class AuthInterceptor(private val authorizationRepository: AuthorizationRepository) : Interceptor {

    // 创建一个锁对象,假设你对所有请求使用相同的拦截器实例,或者创建一个静态锁
    private val reentrantLock = ReentrantLock(true)

    override fun intercept(chain: Interceptor.Chain): Response {

        val originalRequest = chain.request()
        val userIsNotAuthorize = runBlocking { !authorizationRepository.userIsAuthorize().first() }
        if (userIsNotAuthorize) {
            return chain.proceed(originalRequest)
        }

        val accessToken = authorizationRepository.getAccessToken()
        val tokenHasExpired = authorizationRepository.tokenHasExpired()

        return if (tokenHasExpired) {
            chain.proceedDeletingTokenOnError(
                originalRequest.newBuilder()
                    .addHeaders(accessToken)
                    .build()
            )
        } else {
            // 在刷新之前加锁
            reentrantLock.lock()
            // 在进入锁定块时,验证之前的请求是否已经更新了令牌
            val recentAccessToken = authorizationRepository.getAccessToken()
            val recentTokenHasExpired = authorizationRepository.tokenHasExpired()
            if(recentTokenHasExpired) {
                return chain.proceed(
                    originalRequest.newBuilder()
                    .addHeaders(accessToken)
                    .build())
            }
            // 如果没有刷新,则在锁定内刷新
                val refreshTokenRequest = originalRequest.newBuilder()
                    .post("".toRequestBody("application/json".toMediaType()))
                    .url(BuildConfig.BASE_URL + "/v1/user/refresh")
                    .addHeaders(accessToken)
                    .build()
                val refreshResponse = chain.proceedDeletingTokenOnError(refreshTokenRequest)

                refreshResponse.use {
                    if (it.isSuccessful) {
                        val refreshedToken = Gson().fromJson(
                            it.body?.string(),
                            AuthorizedUserDto::class.java
                        )
                        authorizationRepository.setAccessToken(
                            refreshedToken.items.token,
                            refreshedToken.items.expiredAt
                        )
                        val newCall = originalRequest.newBuilder()
                            .addHeaders(refreshedToken.items.token)
                            .build()
                        chain.proceedDeletingTokenOnError(newCall)
                    } else {
                        chain.proceedDeletingTokenOnError(originalRequest)
                    }
                }
                // 在刷新令牌后解锁,以便队列中等待锁的下一个请求可以使用更新的令牌继续
                reentrantLock.unlock()
        }
    }

    private inline fun Request.Builder.addHeaders(token: String) =
        this.apply { header("Authorization", "Bearer $token") }

    private fun Interceptor.Chain.proceedDeletingTokenOnError(request: Request): Response {
        val response = proceed(request)
        if (response.code == 401) {
            runBlocking { authorizationRepository.changeUserState(UserState.NotAuthorized) }
        }
        return response
    }
}

如果您需要进一步的帮助,请随时告诉我。

英文:

You could handle multiple requests in interceptor using locks, find below code with comments using ReentrantLock

class AuthInterceptor(private val authorizationRepository: AuthorizationRepository) : Interceptor {
//Create a lock object, assuming you are same instance of interceptor for all requests or create a static lock
private val reentrantLock = ReentrantLock(true)
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val userIsNotAuthorize = runBlocking { !authorizationRepository.userIsAuthorize().first() }
if (userIsNotAuthorize) {
return chain.proceed(originalRequest)
}
val accessToken = authorizationRepository.getAccessToken()
val tokenHasExpired = authorizationRepository.tokenHasExpired()
return if (tokenHasExpired) {
chain.proceedDeletingTokenOnError(
originalRequest.newBuilder()
.addHeaders(accessToken)
.build()
)
} else {
//lock before refreshing
reentrantLock.lock()
//verify if previous request already updated the token while entering the locked block
val recentAccessToken = authorizationRepository.getAccessToken()
val recentTokenHasExpired = authorizationRepository.tokenHasExpired()
if(recentTokenHasExpired) {
return chain.proceed(
originalRequest.newBuilder()
.addHeaders(accessToken)
.build())
}
//if not refreshed refresh inside lock
val refreshTokenRequest = originalRequest.newBuilder()
.post("".toRequestBody("application/json".toMediaType()))
.url(BuildConfig.BASE_URL + "/v1/user/refresh")
.addHeaders(accessToken)
.build()
val refreshResponse = chain.proceedDeletingTokenOnError(refreshTokenRequest)
refreshResponse.use {
if (it.isSuccessful) {
val refreshedToken = Gson().fromJson(
it.body?.string(),
AuthorizedUserDto::class.java
)
authorizationRepository.setAccessToken(
refreshedToken.items.token,
refreshedToken.items.expiredAt
)
val newCall = originalRequest.newBuilder()
.addHeaders(refreshedToken.items.token)
.build()
chain.proceedDeletingTokenOnError(newCall)
} else {
chain.proceedDeletingTokenOnError(originalRequest)
}
}
// unlock after refreshing token, so next request 
// in queue waiting for lock would proceed with updated token
reentrantLock.unlock()
}
}
private inline fun Request.Builder.addHeaders(token: String) =
this.apply { header("Authorization", "Bearer $token") }
private fun Interceptor.Chain.proceedDeletingTokenOnError(request: Request): Response {
val response = proceed(request)
if (response.code == 401) {
runBlocking { authorizationRepository.changeUserState(UserState.NotAuthorized) }
}
return response
}
}

huangapple
  • 本文由 发表于 2023年5月21日 02:53:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/76296878.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定