Okhttp Authenticator多线程

时间:2015-09-02 13:08:48

标签: android multithreading authentication okhttp

我在我的Android应用程序中使用OkHttp并发出了几个异步请求。所有请求都需要使用标头发送令牌。有时我需要使用RefreshToken刷新令牌,所以我决定使用OkHttp' Authenticator类。

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

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

如何只刷新一次令牌?

5 个答案:

答案 0 :(得分:7)

  1. 使用单例Authenticator

  2. 确保用于操作令牌的方法为Synchronized

  3. 计算重试次数以防止刷新次数过多 令牌呼叫

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

这里是科特林的一个样本

@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 ")
    }
}

答案 1 :(得分:2)

我在这里看到两个基于你调用的API如何工作的场景。

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

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

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

正如您已经注意到,通过跟踪收到401响应的请求,有额外的检查if(!stateObject.isBeingRefreshed) return;取消请求新凭据。

答案 2 :(得分:1)

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

如果令牌相同,这意味着它还没有被刷新,所以我再次执行令牌刷新和当前请求。

如果令牌不相同,这意味着它之前已刷新,所以我再次执行请求,但使用设备中保存的令牌。

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

答案 3 :(得分:0)

这是我的解决方案,可确保在多线程情况下使用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()) {
            synchronized(this) {
                if (!isTokenValid()) { // Double check if another thread already saved a token
                    val jwt = retrieveToken() // HTTP call to get token
                    saveToken(jwt)
                }
            }
        }
        return originalRequest.newBuilder()
                .header("Authorization", getToken())
                .build()
    }

}

您甚至可以为这种情况编写单元测试!

答案 4 :(得分:0)

添加 已同步 来进行authenticate()方法签名。

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

@Nullable
@Override
public synchronized Request authenticate(Route route, Response response) {

    String newAccessToken = getToken();

    return response.request().newBuilder()
            .header("Authorization", "Bearer " + newAccessToken)
            .build();
}