简体   繁体   English

将协程运行块与身份验证器一起使用以处理来自 retrofit 的 401 响应

[英]Using Coroutine runblock with the Authenticator to handle 401 response from retrofit

I am trying to use the Authenticator to handle 401 response.我正在尝试使用 Authenticator 来处理 401 响应。 What I have done is我所做的是

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()
            }
        }
    }

Now, it gets called when there is a 401 response.现在,它会在有 401 响应时被调用。 The refresh token call is also fired (from Log).刷新令牌调用也被触发(从日志)。 However, it never gets the result in the refreshTokenResponse and nothing happens after that.但是,它永远不会在 refreshTokenResponse 中得到结果,之后什么也没有发生。 I think its a wrong way of using runBlock.我认为这是一种错误的使用 runBlock 的方式。 The api is 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>

Any help would be really appreciated.任何帮助将非常感激。 Thanks谢谢

In the Retrofit API, consider replacing your async runBlocking{} suspend fun with a synchronous Call .在 Retrofit API 中,考虑用同步Call替换您的 async runBlocking{} suspend fun 。 I had the most luck avoiding the use of coroutines inside the Authenticator.我最幸运地避免了在 Authenticator 中使用协程。

I was having the same problem.我遇到了同样的问题。 The token request went straight into a black hole.令牌请求直接进入黑洞。 The app froze.应用程序冻结了。 The request was never seen again.该请求再也没有出现过。 No error, no nothing.没有错误,什么都没有。

But everywhere else in the app, the suspend fun came back just fine.但是在应用程序的其他任何地方,暂停的乐趣都恢复得很好。 From ViewModels, from WorkManager, it worked every time.从 ViewModels,从 WorkManager,它每次都能正常工作。 But from the Authenticator, never.但是从 Authenticator 那里,从来没有。 What was wrong with the Authenticator? Authenticator 出了什么问题? What was special about the Authenticator that made it act this way?使它以这种方式运行的 Authenticator 有什么特别之处?

Then I replaced the runBlocking{} coroutine with a straightforward Call .然后我将 runBlocking{} 协程替换为简单的Call This time, the request came back and the token arrived without a fuss.这一次,请求回来了,令牌也毫不费力地到了。

The way I got the API to work looked like this:我让 API 工作的方式如下所示:

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

Then, in the Authenticator:然后,在身份验证器中:

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

I hope this helps someone else who ran into the same issue.我希望这可以帮助遇到同样问题的其他人。 You may receive a warning from Android Studio that this is an inappropriate blocking call.您可能会收到来自 Android Studio 的警告,这是一个不恰当的阻塞调用。 Ignore it.忽略它。

  • Refresh token only once for multiple requests多个请求只刷新一次令牌
  • Log out user if refreshToken failed如果 refreshToken 失败,则注销用户
  • Log out if user gets an error after first refreshing如果用户在第一次刷新后出现错误,请注销
  • Queue all requests while token is being refreshed在刷新令牌时将所有请求排队

https://github.com/hoc081098/Refresh-Token-Sample/blob/master/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt 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)
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM