【问题标题】:Okhttp Authenticator multithreadingOkhttp Authenticator 多线程
【发布时间】:2015-11-28 00:25:08
【问题描述】:

我在我的 android 应用程序中使用 OkHttp 并带有几个异步请求。所有请求都需要与标头一起发送令牌。有时我需要使用 RefreshToken 刷新令牌,所以我决定使用 OkHttpAuthenticator 类。

当 2 个或多个异步请求同时从服务器获得 401 响应码时会发生什么? Authenticator 的authenticate() 方法会为每个请求调用,还是只会为第一个获得 401 的请求调用一次?

@Override
public Request authenticate(Proxy proxy, Response response) throws IOException
{                
    return null;
}

如何只刷新一次token?

【问题讨论】:

    标签: android multithreading authentication okhttp


    【解决方案1】:
    1. 使用单例Authenticator

    2. 确保您用来操作令牌的方法是Synchronized

    3. 计算重试次数,防止刷新次数过多 令牌调用

    4. 确保 API 调用以获取新令牌,并且 将新令牌保存在本地存储中的本地存储事务不是异步的。或者,如果您想让它们异步,请确保在它们完成后向您提供与令牌相关的内容。
    5. 检查访问令牌是否已被另一个线程刷新到 避免从后端请求新的访问令牌

    这是 Kotlin 中的示例

    @SingleTon
    class TokenAuthenticator @Inject constructor(
        private val tokenRepository: TokenRepository
    ) : Authenticator {
        override fun authenticate(route: Route?, response: Response): Request? {
            return if (isRequestRequiresAuth(response)) {
                val request = response.request()
                authenticateRequestUsingFreshAccessToken(request, retryCount(request) + 1)
            } else {
                null
            }
        }
    
        private fun retryCount(request: Request): Int =
            request.header("RetryCount")?.toInt() ?: 0
    
        @Synchronized
        private fun authenticateRequestUsingFreshAccessToken(
            request: Request,
            retryCount: Int
        ): Request? {
            if (retryCount > 2) return null
    
            tokenRepository.getAccessToken()?.let { lastSavedAccessToken ->
                val accessTokenOfRequest = request.header("Authorization") // Some string manipulation needed here to get the token if you have a Bearer token
    
                if (accessTokenOfRequest != lastSavedAccessToken) {
                    return getNewRequest(request, retryCount, lastSavedAccessToken)
                }
            }
    
            tokenRepository.getFreshAccessToken()?.let { freshAccessToken ->
                return getNewRequest(request, retryCount, freshAccessToken)
            }
    
            return null
        }
    
        private fun getNewRequest(request: Request, retryCount: Int, accessToken: String): Request {
            return request.newBuilder()
                .header("Authorization", "Bearer " + accessToken)
                .header("RetryCount", "$retryCount")
                .build()
        }
    
        private fun isRequestRequiresAuth(response: Response): Boolean {
            val header = response.request().header("Authorization")
            return header != null && header.startsWith("Bearer ")
        }
    }
    

    【讨论】:

    • 这段代码不是假设 original 请求在执行任何操作之前已经有一个 Authorization 标头吗?您需要应用代码来提供吗?
    • @mabi 我不知道这是否是您的意思,但您可以尝试使用拦截器来区分需要授权标头的端点并仅对这些端点应用 TokenAuthenticator。
    • 我理解的方式是 OkHttp 将在 401/403 响应上调用 authenticate,然后您在 isRequestRequiresAuth 中测试 Authorization 标头,如果没有则返回 null。因此,如果原始请求还没有 Authorization 标头,您会中止请求吗?
    • 根据文档,authenticate 方法只会对 HTTP 401 未经授权的响应做出反应。如果您的原始请求无法响应身份验证需求,则 authenticate 方法将返回 null。
    【解决方案2】:

    根据您调用的 API 的工作原理,我在这里看到了两种情况。

    第一个肯定更容易处理 - 调用新凭据(例如访问令牌)不会使旧凭据过期。要实现它,您可以在凭据中添加一个额外的标志,表示正在刷新凭据。当您收到 401 响应时,您将 flag 设置为 true,请求获取新凭据,并且仅当 flag 等于 true 时才保存它们,因此只会处理第一个响应,其余的将被忽略。确保您对标志的访问是同步的。

    另一种情况有点棘手 - 每次调用新凭据时,旧凭据都会被服务器端设置为过期。为了处理它,我将引入新对象用作信号前 - 每次“刷新凭据”时它都会被阻止。为确保您只进行一次“刷新凭据”调用,您需要在与标志同步的代码块中调用它。它可以看起来像这样:

    synchronized(stateObject) {
       if(!stateObject.isBeingRefreshed) return;
       Response response = client.execute(request);
       apiClient.setCredentials(response.getNewCredentials());
       stateObject.isBeingRefreshed = false;
    }
    

    正如您所注意到的,有一个额外的检查 if(!stateObject.isBeingRefreshed) return; 以通过遵循收到 401 响应的请求来取消请求新凭据。

    【讨论】:

    • 那样,您会丢失返回的请求,不是吗?使用我在下面的方式,您可以再次调用该请求。
    • 您指的是哪种情况?如果服务器在生成新的访问令牌时没有使旧的访问令牌过期,那么您不需要阻止它,并且当我们丢失任何令牌时这不是问题,因为我们仍然有一个可以工作的访问令牌。当然,它保护您的系统如何工作。在第二种情况下,我首先描述了如何防止调用更多“刷新访问令牌”请求(同步块将阻止这样做),因此不会丢失任何内容。
    • 是的,如果服务器没有过期,旧的访问权限就不需要阻止旧的请求。谢谢你的肯定。
    【解决方案3】:

    这是我的解决方案,确保在多线程情况下只刷新一次令牌,使用okhttp3.Authenticator

    class Reauthenticator : Authenticator {
    
        override fun authenticate(route: Route?, response: Response?): Request? {
            if (response == null) return null
            val originalRequest = response.request()
            if (originalRequest.header("Authorization") != null) return null // Already failed to authenticate
            if (!isTokenValid()) { // Check if token is saved locally
                synchronized(this) {
                    if (!isTokenValid()) { // Double check if another thread already saved a token locally
                        val jwt = retrieveToken() // HTTP call to get token
                        saveToken(jwt)
                    }
                }
            }
            return originalRequest.newBuilder()
                    .header("Authorization", getToken())
                    .build()
        }
    
    }
    

    您甚至可以为此案例编写单元测试! ?

    【讨论】:

    • 函数isTokenValid()有什么作用?每次都会访问服务器以检查令牌是否仍然有效?
    • 感谢您的提问,我知道这并不明显。这只是一个本地检查,实施将取决于您的具体情况。我已经对代码进行了注释以使其更清楚。
    • 我迟到了,但是有一个问题:当你的函数被调用一个带有不记名令牌的请求时,这意味着 okhttp 已经尝试了该请求并且它失败了。在这种情况下,您不想获取一个新的令牌而不是让请求落地吗?
    【解决方案4】:

    在我的例子中,我使用单例模式实现了Authenticator。您可以同步该方法authenticate。在他的实现中,我检查请求中的令牌(从在 authenticate 方法的参数中收到的 Response 对象获取 Request 对象)是否与保存在设备中的相同(我将令牌保存在 @987654325 @ 目的)。

    如果token相同,说明还没有刷新,所以我再次执行token刷新和当前请求。

    如果token不一样,说明之前刷新过,所以我再次执行请求,但使用设备中保存的token。

    如果您需要更多帮助,请告诉我,我会在这里放一些代码。

    【讨论】:

    • 我认为最好通过“提交”方法而不是“应用”方法添加令牌需要保存的信息以使其同步。
    • 嗨@antonicg。你能提供一些代码吗?我试图找到一种方法来处理不同线程可能同时尝试刷新的实例。听起来您的回答可以解决我的问题,但是同步是我卡住并且不太明白的地方。
    • 嗨@JPM 我现在没有这个代码。但是您必须在类上实现接口 Authenticator 并注意方法身份验证是同步的。如果您将该类配置为 Singleton,则您只有一个实例,并且只有一个线程有时会访问同步方法。您在刷新令牌时设置一个标志或其他内容,您将实现它。
    • 不用担心@antonicg!我剩下的唯一问题是您是否建议我在同步身份验证方法中需要一些锁?一旦我将方法标记为同步,我就印象深刻,我不需要锁或类似的东西,因为它会暂停执行,直到当前线程退出该方法。我的身份验证器也使用共享首选项,所以我认为您将请求令牌与保存的内容进行比较的建议就足够了?
    • @JPM 是的,你是对的,如果你将方法标记为同步,你就不需要任何锁
    【解决方案5】:

    同步添加到 authenticate() 方法签名中。

    并确保 getToken() 方法被阻塞。

    @Nullable
    @Override
    public synchronized Request authenticate(Route route, Response response) {
    
        String newAccessToken = getToken();
    
        return response.request().newBuilder()
                .header("Authorization", "Bearer " + newAccessToken)
                .build();
    }
    

    【讨论】:

      【解决方案6】:

      确保使用单例自定义身份验证器 使用新令牌刷新令牌成功返回请求时,否则返回null。

      class TokenAuthenticator(
      private val sharedPref: SharedPref,
      private val tokenRefreshApi: TokenRefreshApi
      

      ) :身份验证器, SafeApiCall {

      override fun authenticate(route: Route?, response: Response): Request? {
          return runBlocking {
              when (val tokenResponse = getUpdatedToken()) {
                  is Resource.Success -> {
                      val token = tokenResponse.data.token
                      sharedPref.saveToken(token)
                      response.request.newBuilder().header("Authorization", "Bearer $token").build()
                  }
                  else -> {
                      null
                  }
              }
          }
      }
      
      private suspend fun getUpdatedToken(): Resource<LoginResponse> {
          return safeApiCall { tokenRefreshApi.refreshToken("Bearer ${sharedPref.getToken()}") }
      }
      

      }

      【讨论】:

        猜你喜欢
        • 2020-01-12
        • 2019-09-02
        • 2016-07-09
        • 1970-01-01
        • 2020-04-05
        • 2021-11-30
        • 2021-03-16
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多