问题
I am using OkHttp in my android application with several async requests. All requests require a token to be sent with the header. Sometimes I need to refresh the token using a RefreshToken, so I decided to use OkHttp's Authenticator class.
What will happen when 2 or more async requests get a 401 response code from the server at the same time? Would the Authenticator's authenticate() method be called for each request, or it will only called once for the first request that got a 401?
@Override
public Request authenticate(Proxy proxy, Response response) throws IOException
{
return null;
}
How to refresh token only once?
回答1:
I see here two scenarios based on how API which you call works.
First one is definitely easier to handle - calling new credentials (e.g. access token) doesn't expire old one. To achieve it you can add an extra flag to your credentials to say that credentials are being refreshed. When you got 401 response, you set flag to true, make a request to get new credentials and you save them only if flag equals true so only first response will be handled and rest of them will be ignored. Make sure that your access to flag is synchronized.
Another scenario is a little bit more tricky - every time when you call new credentials old one are set to be expired by server side. To handle it you I would introduce new object to be used as a semafore - it would be blocked every time when 'credentials are being refreshed'. To make sure that you'll make only one 'refresh credentials' call, you need to call it in block of code which is synchronized with flag. It can look like it:
synchronized(stateObject) {
if(!stateObject.isBeingRefreshed) return;
Response response = client.execute(request);
apiClient.setCredentials(response.getNewCredentials());
stateObject.isBeingRefreshed = false;
}
As you've noticed there is an extra check if(!stateObject.isBeingRefreshed) return; to cancel requesting new credentials by following requests which received 401 response.
回答2:
In my case I implemented the Authenticator using the Singleton pattern. You can made synchronized that method authenticate. In his implementation, I check if the token from the request (getting the Request object from Response object received in the params of authenticate method) is the same that the saved in the device (I save the token in a SharedPreferences object).
If the token is the same, that means that it has not been refresed yet, so I execute the token refresh and the current request again.
If the token is not the same, that means that it has been refreshed before, so I execute the request again but using the token saved in the device.
If you need more help, please tell me and I will put some code here.
回答3:
This is my solution to make sure to refresh token only once in a multi-threading case, using 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()
}
}
You can even write a unit test for this case, too! 🎉
回答4:
Add synchronized to authenticate() method signature.
And make sure getToken() method is blocking.
@Nullable
@Override
public synchronized Request authenticate(Route route, Response response) {
String newAccessToken = getToken();
return response.request().newBuilder()
.header("Authorization", "Bearer " + newAccessToken)
.build();
}
来源:https://stackoverflow.com/questions/32354098/okhttp-authenticator-multithreading