java - example - okhttpclient jar
El token de actualización de Okhttp expiró cuando se envían múltiples solicitudes al servidor (4)
Tengo un ViewPager
y se ViewPager
tres llamadas de servicio web cuando el ViewPager
se carga simultáneamente.
Cuando el primero devuelve 401, se llama a Authenticator
y actualizo el token dentro de Authenticator
, pero las 2 solicitudes restantes ya se han enviado al servidor con el token de actualización anterior y falla con 498, que se captura en Interceptor y la aplicación se desconecta.
Este no es el comportamiento ideal que esperaría. Me gustaría mantener la segunda y tercera solicitud en la cola y cuando se actualice el token, vuelva a intentar la solicitud en cola.
Actualmente, tengo una variable para indicar si la actualización del token está en curso en Authenticator
, en ese caso, cancelo todas las solicitudes posteriores en el Interceptor
y el usuario debe actualizar la página manualmente o puedo cerrar la sesión del usuario y forzar al usuario a iniciar sesión.
¿Qué es una buena solución o arquitectura para el problema anterior utilizando okhttp 3.x para Android?
EDITAR: El problema que quiero resolver es en general y no me gustaría secuenciar mis llamadas. es decir, espere a que finalice una llamada y actualice el token y luego solo envíe el resto de la solicitud en el nivel de actividad y fragmento.
Se solicitó el código. Este es un código estándar para el Authenticator
:
public class CustomAuthenticator implements Authenticator {
@Inject AccountManager accountManager;
@Inject @AccountType String accountType;
@Inject @AuthTokenType String authTokenType;
@Inject
public ApiAuthenticator(@ForApplication Context context) {
}
@Override
public Request authenticate(Route route, Response response) throws IOException {
// Invaidate authToken
String accessToken = accountManager.peekAuthToken(account, authTokenType);
if (accessToken != null) {
accountManager.invalidateAuthToken(accountType, accessToken);
}
try {
// Get new refresh token. This invokes custom AccountAuthenticator which makes a call to get new refresh token.
accessToken = accountManager.blockingGetAuthToken(account, authTokenType, false);
if (accessToken != null) {
Request.Builder requestBuilder = response.request().newBuilder();
// Add headers with new refreshToken
return requestBuilder.build();
} catch (Throwable t) {
Timber.e(t, t.getLocalizedMessage());
}
}
return null;
}
}
Algunas preguntas similares a esto: OkHttp y Retrofit, actualizar token con solicitudes concurrentes
Encontré la solución con el autenticador, el ID es el número de la solicitud, solo para la identificación. Los comentarios estan en español
private final static Lock locks = new ReentrantLock();
httpClient.authenticator(new Authenticator() {
@Override
public Request authenticate(@NonNull Route route,@NonNull Response response) throws IOException {
Log.e("Error" , "Se encontro un 401 no autorizado y soy el numero : " + id);
//Obteniendo token de DB
SharedPreferences prefs = mContext.getSharedPreferences(
BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
String token_db = prefs.getString("refresh_token","");
//Comparando tokens
if(mToken.getRefreshToken().equals(token_db)){
locks.lock();
try{
//Obteniendo token de DB
prefs = mContext.getSharedPreferences(
BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
String token_db2 = prefs.getString("refresh_token","");
//Comparando tokens
if(mToken.getRefreshToken().equals(token_db2)){
//Refresh token
APIClient tokenClient = createService(APIClient.class);
Call<AccessToken> call = tokenClient.getRefreshAccessToken(API_OAUTH_CLIENTID,API_OAUTH_CLIENTSECRET, "refresh_token", mToken.getRefreshToken());
retrofit2.Response<AccessToken> res = call.execute();
AccessToken newToken = res.body();
// do we have an access token to refresh?
if(newToken!=null && res.isSuccessful()){
String refreshToken = newToken.getRefreshToken();
Log.e("Entra", "Token actualizado y soy el numero : " + id + " : " + refreshToken);
prefs = mContext.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
prefs.edit().putBoolean("log_in", true).apply();
prefs.edit().putString("access_token", newToken.getAccessToken()).apply();
prefs.edit().putString("refresh_token", refreshToken).apply();
prefs.edit().putString("token_type", newToken.getTokenType()).apply();
locks.unlock();
return response.request().newBuilder()
.header("Authorization", newToken.getTokenType() + " " + newToken.getAccessToken())
.build();
}else{
//Dirigir a login
Log.e("redirigir", "DIRIGIENDO LOGOUT");
locks.unlock();
return null;
}
}else{
//Ya se actualizo tokens
Log.e("Entra", "El token se actualizo anteriormente, y soy el no : " + id );
prefs = mContext.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
String type = prefs.getString("token_type","");
String access = prefs.getString("access_token","");
locks.unlock();
return response.request().newBuilder()
.header("Authorization", type + " " + access)
.build();
}
}catch (Exception e){
locks.unlock();
e.printStackTrace();
return null;
}
}
return null;
}
});
Es importante tener en cuenta que aún se puede llamar a accountManager.blockingGetAuthToken
(o la versión sin bloqueo) en otra parte, a excepción del interceptor. Por lo tanto, el lugar correcto para evitar que ocurra este problema sería dentro del autenticador.
Queremos asegurarnos de que el primer subproceso que necesita un token de acceso lo recuperará, y es posible que otros subprocesos simplemente se registren para que se invoque una devolución de llamada cuando el primer subproceso haya terminado de recuperar el token.
La buena noticia es que AbstractAccountAuthenticator
ya tiene una forma de entregar resultados asíncronos, es decir, AccountAuthenticatorResponse
, a los que puede llamar onResult
o onError
.
La siguiente muestra consta de 3 bloques.
La primera consiste en asegurarse de que solo un hilo obtenga el token de acceso, mientras que otros hilos solo registran su response
para una devolución de llamada.
La segunda parte es solo un paquete de resultados vacíos ficticios. Aquí, cargarías tu token, posiblemente lo refrescarías, etc.
La tercera parte es lo que haces una vez que tienes tu resultado (o error). Debe asegurarse de llamar a la respuesta para todos los demás subprocesos que puedan haberse registrado.
boolean fetchingToken;
List<AccountAuthenticatorResponse> queue = null;
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
synchronized (this) {
if (fetchingToken) {
// another thread is already working on it, register for callback
List<AccountAuthenticatorResponse> q = queue;
if (q == null) {
q = new ArrayList<>();
queue = q;
}
q.add(response);
// we return null, the result will be sent with the `response`
return null;
}
// we have to fetch the token, and return the result other threads
fetchingToken = true;
}
// load access token, refresh with refresh token, whatever
// ... todo ...
Bundle result = Bundle.EMPTY;
// loop to make sure we don''t drop any responses
for ( ; ; ) {
List<AccountAuthenticatorResponse> q;
synchronized (this) {
// get list with responses waiting for result
q = queue;
if (q == null) {
fetchingToken = false;
// we''re done, nobody is waiting for a response, return
return null;
}
queue = null;
}
// inform other threads about the result
for (AccountAuthenticatorResponse r : q) {
r.onResult(result); // return result
}
// repeat for the case another thread registered for callback
// while we were busy calling others
}
}
Solo asegúrese de devolver el null
en todas las rutas cuando use la response
.
Obviamente, podría usar otros medios para sincronizar esos bloques de código, como atómicos como lo muestra @matrix en otra respuesta. Hice uso de la synchronized
, porque creo que esta es la implementación más fácil de comprender, ya que esta es una gran pregunta y todos deberían estar haciendo esto;)
La muestra anterior es una versión adaptada de un bucle de emisor que se describe aquí , donde se detalla detalladamente sobre la concurrencia. Este blog es una gran fuente si está interesado en cómo funciona RxJava bajo el capó.
Puedes hacerlo:
Agregue esos como miembros de datos:
// these two static variables serve for the pattern to refresh a token
private final static ConditionVariable LOCK = new ConditionVariable(true);
private static final AtomicBoolean mIsRefreshing = new AtomicBoolean(false);
y luego en el método de intercepción:
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Request request = chain.request();
// 1. sign this request
....
// 2. proceed with the request
Response response = chain.proceed(request);
// 3. check the response: have we got a 401?
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
if (!TextUtils.isEmpty(token)) {
/*
* Because we send out multiple HTTP requests in parallel, they might all list a 401 at the same time.
* Only one of them should refresh the token, because otherwise we''d refresh the same token multiple times
* and that is bad. Therefore we have these two static objects, a ConditionVariable and a boolean. The
* first thread that gets here closes the ConditionVariable and changes the boolean flag.
*/
if (mIsRefreshing.compareAndSet(false, true)) {
LOCK.close();
/* we''re the first here. let''s refresh this token.
* it looks like our token isn''t valid anymore.
* REFRESH the actual token here
*/
LOCK.open();
mIsRefreshing.set(false);
} else {
// Another thread is refreshing the token for us, let''s wait for it.
boolean conditionOpened = LOCK.block(REFRESH_WAIT_TIMEOUT);
// If the next check is false, it means that the timeout expired, that is - the refresh
// stuff has failed.
if (conditionOpened) {
// another thread has refreshed this for us! thanks!
// sign the request with the new token and proceed
// return the outcome of the newly signed request
response = chain.proceed(newRequest);
}
}
}
}
// check if still unauthorized (i.e. refresh failed)
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
... // clean your access token and prompt for request again.
}
// returning the response to the original request
return response;
}
De esta manera, solo enviarás 1 solicitud para actualizar el token y luego para cada otro tendrás el token actualizado.
Puedes probar con este interceptor de nivel de aplicación.
private class HttpInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
//Build new request
Request.Builder builder = request.newBuilder();
builder.header("Accept", "application/json"); //if necessary, say to consume JSON
String token = settings.getAccessToken(); //save token of this request for future
setAuthHeader(builder, token); //write current token to request
request = builder.build(); //overwrite old request
Response response = chain.proceed(request); //perform request, here original request will be executed
if (response.code() == 401) { //if unauthorized
synchronized (httpClient) { //perform all 401 in sync blocks, to avoid multiply token updates
String currentToken = settings.getAccessToken(); //get currently stored token
if(currentToken != null && currentToken.equals(token)) { //compare current token with token that was stored before, if it was not updated - do update
int code = refreshToken() / 100; //refresh token
if(code != 2) { //if refresh token failed for some reason
if(code == 4) //only if response is 400, 500 might mean that token was not updated
logout(); //go to login screen
return response; //if token refresh failed - show error to user
}
}
if(settings.getAccessToken() != null) { //retry requires new auth token,
setAuthHeader(builder, settings.getAccessToken()); //set auth token to updated
request = builder.build();
return chain.proceed(request); //repeat request with new token
}
}
}
return response;
}
private void setAuthHeader(Request.Builder builder, String token) {
if (token != null) //Add Auth token to each request if authorized
builder.header("Authorization", String.format("Bearer %s", token));
}
private int refreshToken() {
//Refresh token, synchronously, save it, and return result code
//you might use retrofit here
}
private int logout() {
//logout your user
}
}
Puede establecer un interceptor como este para la instancia de okHttp
Gson gson = new GsonBuilder().create();
OkHttpClient httpClient = new OkHttpClient();
httpClient.interceptors().add(new HttpInterceptor());
final RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(BuildConfig.REST_SERVICE_URL)
.setClient(new OkClient(httpClient))
.setConverter(new GsonConverter(gson))
.setLogLevel(RestAdapter.LogLevel.BASIC)
.build();
remoteService = restAdapter.create(RemoteService.class);
¡¡¡¡Espero que esto ayude!!!!