Okhttp 验证器多线程

问题描述 投票:0回答:6

我在我的 Android 应用程序中使用

OkHttp
来处理几个异步请求。所有请求都需要与标头一起发送令牌。有时我需要使用 RefreshToken 刷新令牌,所以我决定使用
OkHttp
Authenticator
类。

当 2 个或更多异步请求同时从服务器获取 401 响应代码时会发生什么? Authenticator 的

authenticate()
方法会针对每个请求调用,还是只会针对第一个收到 401 的请求调用一次?

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

如何只刷新一次令牌?

android multithreading authentication okhttp
6个回答
15
投票
  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 ")
    }
}

4
投票

我在这里看到两个基于您调用的 API 工作方式的场景。

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

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

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

正如您所注意到的,有一个额外的检查

if(!stateObject.isBeingRefreshed) return;
,可以通过遵循收到 401 响应的请求来取消请求新凭据。


3
投票

在我的例子中,我使用单例模式实现了

Authenticator
。您可以同步该方法
authenticate
。在他的实现中,我检查请求中的令牌(从身份验证方法的参数中收到的
Request
对象获取
Response
对象)是否与设备中保存的令牌相同(我将令牌保存在
SharedPreferences 
对象)。

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

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

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


2
投票

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

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

}

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


0
投票

synchronized 添加到authenticate() 方法签名。

并确保 getToken() 方法是阻塞的。

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

    String newAccessToken = getToken();

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

0
投票

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

class TokenAuthenticator(
    private val sharedPref: SharedPref,
    private val tokenRefreshApi: TokenRefreshApi
) : Authenticator,
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()}") }
}
}
© www.soinside.com 2019 - 2024. All rights reserved.