我在我的 Android 应用程序中使用
OkHttp
来处理几个异步请求。所有请求都需要与标头一起发送令牌。有时我需要使用 RefreshToken 刷新令牌,所以我决定使用 OkHttp
的 Authenticator
类。
当 2 个或更多异步请求同时从服务器获取 401 响应代码时会发生什么? Authenticator 的
authenticate()
方法会针对每个请求调用,还是只会针对第一个收到 401 的请求调用一次?
@Override
public Request authenticate(Proxy proxy, Response response) throws IOException
{
return null;
}
如何只刷新一次令牌?
使用单例
Authenticator
确保您用来操作代币的方法是
Synchronized
统计重试次数,防止刷新次数过多 代币调用
这是 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 ")
}
}
我在这里看到两个基于您调用的 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 响应的请求来取消请求新凭据。
在我的例子中,我使用单例模式实现了
Authenticator
。您可以同步该方法authenticate
。在他的实现中,我检查请求中的令牌(从身份验证方法的参数中收到的 Request
对象获取 Response
对象)是否与设备中保存的令牌相同(我将令牌保存在 SharedPreferences
对象)。
如果token相同,说明还没有刷新,所以我再次执行token刷新和当前请求。
如果令牌不相同,则意味着之前已经刷新过,所以我再次执行请求,但使用设备中保存的令牌。
如果您需要更多帮助,请告诉我,我会在这里放一些代码。
这是我的解决方案,以确保在多线程情况下仅刷新令牌一次,使用
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()
}
}
您甚至还可以为此案例编写单元测试! 🎉
将 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();
}
确保使用单例自定义验证器 当刷新令牌成功时,使用新令牌返回请求,否则返回 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()}") }
}
}