【问题标题】:Using Coroutine runblock with the Authenticator to handle 401 response from retrofit使用 Coroutine runblock 和 Authenticator 来处理来自改造的 401 响应
【发布时间】:2020-11-07 01:14:02
【问题描述】:

我正在尝试使用 Authenticator 来处理 401 响应。我所做的是

fun provideAccessTokenAuthenticator(
    mainApiServiceHolder: MainApiServiceHolder,
    preferences: SharedPreferences
) = object : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        val accessToken = preferences.getString(ACCESS_TOKEN, null)
        if (!isRequestWithAccessToken(response) || accessToken == null) {
            return null
        }
        synchronized(this) {
            val newAccessToken = preferences.getString(ACCESS_TOKEN, null)!!
            // Access token is refreshed in another thread.
            if (accessToken != newAccessToken) {
                return newRequestWithAccessToken(response.request, newAccessToken)
            }

            // Need to refresh an access token
            val refreshTokenResponse = runBlocking {
                Log.d("zzzzzzzzzz", "refresh token is running")
                mainApiServiceHolder.mainApiService?.refreshToken(
                    "refresh_token",
                    preferences.getString(REFRESH_TOKEN, null)!!,
                    AuthRepository.CLIENT_ID,
                    AuthRepository.CLIENT_SECRET
                )
            }
            Log.d("zzzzzzzzzz", refreshTokenResponse?.body()?.access_token!!)
            return if (refreshTokenResponse?.isSuccessful!!) {
                Log.d("zzzzzzzzzz", "refresh token is successful")
                newRequestWithAccessToken(
                    response.request,
                    refreshTokenResponse.body()?.access_token!!
                )
            } else {
                Log.d("zzzzzzzzzz", "refresh token is unsuccessful")
                response.request.newBuilder().header("Content-Type", "application/json").build()
            }
        }
    }

现在,当有 401 响应时会调用它。刷新令牌调用也被触发(从日志)。但是,它永远不会在 refreshTokenResponse 中得到结果,之后什么也没有发生。我认为这是使用 runBlock 的错误方式。该api是

@FormUrlEncoded
@POST("/api/auth/token/")
suspend fun refreshToken(
    @Field("grant_type") grant_type: String,
    @Field("refresh_token") refresh_token: String,
    @Field("client_id") client_id: String,
    @Field("client_secret") client_secret: String
): Response<LoginResponse>

任何帮助将不胜感激。谢谢

【问题讨论】:

    标签: android retrofit2 okhttp kotlin-coroutines


    【解决方案1】:
    • 多次请求只刷新一次令牌
    • 如果 refreshToken 失败则注销用户
    • 如果用户在第一次刷新后出现错误,请退出
    • 在刷新令牌时将所有请求排队

    https://github.com/hoc081098/Refresh-Token-Sample/blob/master/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt

    
    class AuthInterceptor @Inject constructor(
      private val userLocalSource: UserLocalSource,
      private val apiService: Provider<ApiService>,
    ) : Interceptor {
      private val mutex = Mutex()
    
      override fun intercept(chain: Interceptor.Chain): Response {
        val req = chain.request().also { Timber.d("[1] $it") }
    
        if (NO_AUTH in req.headers.values(CUSTOM_HEADER)) {
          return chain.proceedWithToken(req, null)
        }
    
        val token =
          runBlocking { userLocalSource.user().first() }?.token.also { Timber.d("[2] $req $it") }
        val res = chain.proceedWithToken(req, token)
    
        if (res.code != HTTP_UNAUTHORIZED || token == null) {
          return res
        }
    
        Timber.d("[3] $req")
    
        val newToken: String? = runBlocking {
          mutex.withLock {
            val user =
              userLocalSource.user().first().also { Timber.d("[4] $req $it") }
            val maybeUpdatedToken = user?.token
    
            when {
              user == null || maybeUpdatedToken == null -> null.also { Timber.d("[5-1] $req") } // already logged out!
              maybeUpdatedToken != token -> maybeUpdatedToken.also { Timber.d("[5-2] $req") } // refreshed by another request
              else -> {
                Timber.d("[5-3] $req")
    
                val refreshTokenRes =
                  apiService.get().refreshToken(RefreshTokenBody(user.refreshToken, user.username))
                    .also {
                      Timber.d("[6] $req $it")
                    }
    
                val code = refreshTokenRes.code()
                if (code == HTTP_OK) {
                  refreshTokenRes.body()?.token?.also {
                    Timber.d("[7-1] $req")
                    userLocalSource.save(
                      user.toBuilder()
                        .setToken(it)
                        .build()
                    )
                  }
                } else if (code == HTTP_UNAUTHORIZED) {
                  Timber.d("[7-2] $req")
                  userLocalSource.save(null)
                  null
                } else {
                  Timber.d("[7-3] $req")
                  null
                }
              }
            }
          }
        }
    
        return if (newToken !== null) chain.proceedWithToken(req, newToken) else res
      }
    
      private fun Interceptor.Chain.proceedWithToken(req: Request, token: String?): Response =
        req.newBuilder()
          .apply {
            if (token !== null) {
              addHeader("Authorization", "Bearer $token")
            }
          }
          .removeHeader(CUSTOM_HEADER)
          .build()
          .let(::proceed)
    }
    

    【讨论】:

      【解决方案2】:

      在 Retrofit API 中,考虑用同步 Call 替换您的异步 runBlocking{} 暂停乐趣。我最幸运的是避免在 Authenticator 中使用协程。

      我遇到了同样的问题。令牌请求直接进入了黑洞。应用程序冻结了。该请求再也没有出现。没有错误,什么都没有。

      但在应用程序的其他任何地方,暂停乐趣都恢复得很好。从 ViewModels 到 WorkManager,它每次都有效。但是从 Authenticator 那里,永远不会。 Authenticator 出了什么问题? Authenticator 有什么特别之处让它如此行事?

      然后我用一个简单的 Call 替换了 runBlocking{} 协程。这一次,请求回来了,令牌也毫不费力地到达了。

      我让 API 工作的方式如下所示:

      @FormUrlEncoded
      @POST("token")
      fun refreshTokenSync(
          @Field("refresh_token") refreshToken: String,
      ): Call<RefreshMyTokenResponse>
      

      然后,在 Authenticator 中:

       val call = API.refreshTokenSync(refreshToken)
       val response = call.execute().body()
      

      我希望这可以帮助遇到同样问题的其他人。您可能会收到来自 Android Studio 的警告,指出这是一个不恰当的阻塞调用。没关系,不要付钱。

      【讨论】:

      • 我无法告诉你我多么欣赏这条评论。几个月来我一直在努力解决这个问题,但无法得到很好的解释。
      • 这是最简单的方法;我还在“刷新过期令牌”部分找到了这篇文章lordcodes.com/articles/…,它支持使用呼叫。尽管他们确实提到您可以使用 KtCoroutines runBlocking。
      • 不过,我不明白为什么同步函数签名会排除“桥接阻塞和非阻塞世界”并在非暂停乐趣中调用暂停乐趣。
      • 我也不是 100% 确定可以使用 Kotlin runBlocking,因为它一直对我造成破坏。
      猜你喜欢
      • 2020-10-05
      • 2021-10-26
      • 2017-06-07
      • 1970-01-01
      • 2021-04-05
      • 1970-01-01
      • 1970-01-01
      • 2021-03-26
      • 1970-01-01
      相关资源
      最近更新 更多