我在我的Android应用程序中使用OkHttp
并发出了几个异步请求。所有请求都需要使用标头发送令牌。有时我需要使用RefreshToken刷新令牌,所以我决定使用OkHttp
' Authenticator
类。
当两个或多个异步请求同时从服务器获得401响应代码时会发生什么?是否会为每个请求调用Authenticator的authenticate()
方法,或者只为第一个获得401的请求调用一次?
@Override
public Request authenticate(Proxy proxy, Response response) throws IOException
{
return null;
}
如何只刷新一次令牌?
答案 0 :(得分:7)
使用单例Authenticator
确保用于操作令牌的方法为Synchronized
计算重试次数以防止刷新次数过多 令牌呼叫
这里是科特林的一个样本
@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();
}