Refrescar el token de OAuth utilizando Retrofit sin modificar todas las llamadas

Estamos utilizando Retrofit en nuestra aplicación de Android, para comunicarnos con un servidor protegido OAuth2. Todo funciona muy bien, utilizamos RequestInterceptor para incluir el token de acceso con cada llamada. Sin embargo, habrá veces, cuando el token de acceso expire, y el token necesita ser actualizado. Cuando el token caduca, la próxima llamada volverá con un código HTTP no autorizado, por lo que es fácil de supervisar. Podríamos modificar cada llamada de Retrofit de la siguiente manera: En el fallo de devolución de llamada, compruebe el código de error, si es igual a No autorizado, actualice el token de OAuth y repita la llamada de Retrofit. Sin embargo, para ello, todas las llamadas deben ser modificadas, lo que no es una solución fácil de mantener y buena. ¿Hay una manera de hacer esto sin modificar todas las llamadas de Retrofit?

Por favor, no utilice Interceptors para hacer frente a la autenticación.

El mejor enfoque actual para manejar la autenticación es utilizar la nueva API Authenticator , diseñada específicamente para este propósito .

OkHttp solicitará automáticamente al Authenticator credenciales cuando una respuesta es 401 Not Authorised reintentando la última solicitud con error.

 public class TokenAuthenticator implements Authenticator { @Override public Request authenticate(Proxy proxy, Response response) throws IOException { // Refresh your access_token using a synchronous api request newAccessToken = service.refreshToken(); // Add new header to rejected request and retry it return response.request().newBuilder() .header(AUTHORIZATION, newAccessToken) .build(); } @Override public Request authenticateProxy(Proxy proxy, Response response) throws IOException { // Null indicates no attempt to authenticate. return null; } 

Adjunte un Authenticator a un OkHttpClient la misma manera que lo hace con Interceptors

 OkHttpClient okHttpClient = new OkHttpClient(); okHttpClient.setAuthenticator(authAuthenticator); 

Utilice este cliente al crear su Retrofit RestAdapter

 RestAdapter restAdapter = new RestAdapter.Builder() .setEndpoint(ENDPOINT) .setClient(new OkClient(okHttpClient)) .build(); return restAdapter.create(API.class); 

Si está usando Retrofit > = 1.9.0 entonces podría hacer uso del nuevo Interceptor de OkHttp , que fue introducido en OkHttp 2.2.0 . Desea utilizar un interceptor de aplicación , que le permite retry and make multiple calls .

Su Interceptor podría ser similar a este pseudocódigo:

 public class CustomInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); // try the request Response response = chain.proceed(request); if (response shows expired token) { // get a new token (I use a synchronous Retrofit call) // create a new request and modify it accordingly using the new token Request newRequest = request.newBuilder()...build(); // retry the request return chain.proceed(newRequest); } // otherwise just pass the original response on return response; } } 

Después de definir su Interceptor , cree un OkHttpClient y agregue el interceptor como Interceptor de Aplicación .

  OkHttpClient okHttpClient = new OkHttpClient(); okHttpClient.interceptors().add(new CustomInterceptor()); 

Y, finalmente, utilizar este OkHttpClient al crear su RestAdapter .

  RestService restService = new RestAdapter().Builder ... .setClient(new OkClient(okHttpClient)) .create(RestService.class); 

Advertencia: Como Jesse Wilson (de Square) menciona aquí , esta es una cantidad peligrosa de poder.

Dicho esto, definitivamente creo que esta es la mejor manera de manejar algo como esto ahora. Si tiene alguna pregunta no dude en preguntar en un comentario.

TokenAuthenticator depende de una clase de servicio. La clase de servicio depende de una instancia OkHttpClient. Para crear un OkHttpClient necesito el TokenAuthenticator. ¿Cómo puedo romper este ciclo? ¿Dos OkHttpClients diferentes? Ellos van a tener diferentes piscinas de conexión ..

Si tiene, por ejemplo, un TokenService Retrofit que necesita dentro de su Authenticator pero sólo desea configurar un OkHttpClient , puede usar TokenServiceHolder como una dependencia para TokenAuthenticator . Usted tendría que mantener una referencia a él en el nivel de la aplicación (singleton). Esto es fácil si está usando Dagger 2, de lo contrario sólo cree el campo de clase dentro de su aplicación.

En TokenAuthenticator.java

 public class TokenAuthenticator implements Authenticator { private final TokenServiceHolder tokenServiceHolder; public TokenAuthenticator(TokenServiceHolder tokenServiceHolder) { this.tokenServiceHolder = tokenServiceHolder; } @Override public Request authenticate(Proxy proxy, Response response) throws IOException { //is there a TokenService? TokenService service = tokenServiceHolder.get(); if (service == null) { //there is no way to answer the challenge //so return null according to Retrofit's convention return null; } // Refresh your access_token using a synchronous api request newAccessToken = service.refreshToken().execute(); // Add new header to rejected request and retry it return response.request().newBuilder() .header(AUTHORIZATION, newAccessToken) .build(); } @Override public Request authenticateProxy(Proxy proxy, Response response) throws IOException { // Null indicates no attempt to authenticate. return null; } 

En TokenServiceHolder.java :

 public class TokenServiceHolder { TokenService tokenService = null; @Nullable public TokenService get() { return tokenService; } public void set(TokenService tokenService) { this.tokenService = tokenService; } } 

Configuración del cliente:

 //obtain instance of TokenServiceHolder from application or singleton-scoped component, then TokenAuthenticator authenticator = new TokenAuthenticator(tokenServiceHolder); OkHttpClient okHttpClient = new OkHttpClient(); okHttpClient.setAuthenticator(tokenAuthenticator); Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com/") .client(okHttpClient) .build(); TokenService tokenService = retrofit.create(TokenService.class); tokenServiceHolder.set(tokenService); 

Si está usando Dagger 2 o un marco de inyección de dependencia similar hay algunos ejemplos en las respuestas a esta pregunta

Puede intentar crear una clase base para todos sus cargadores en la que sería capaz de detectar una excepción en particular y luego actuar según sus necesidades. Haga que todos sus diferentes cargadores se extiendan desde la clase base para poder propagar el comportamiento.

Después de una larga investigación, personalizé cliente Apache para manejar Refreshing AccessToken Para Retrofit En el que envía el token de acceso como parámetro.

Inicie su adaptador con cookie Persistent Client

 restAdapter = new RestAdapter.Builder() .setEndpoint(SERVER_END_POINT) .setClient(new CookiePersistingClient()) .setLogLevel(RestAdapter.LogLevel.FULL).build(); 

Cookie Cliente persistente que mantiene las cookies para todas las solicitudes y cheques con cada respuesta de solicitud, si es un acceso no autorizado ERROR_CODE = 401, refresca el token de acceso y recupera la solicitud, o simplemente procesa la solicitud.

 private static class CookiePersistingClient extends ApacheClient { private static final int HTTPS_PORT = 443; private static final int SOCKET_TIMEOUT = 300000; private static final int CONNECTION_TIMEOUT = 300000; public CookiePersistingClient() { super(createDefaultClient()); } private static HttpClient createDefaultClient() { // Registering https clients. SSLSocketFactory sf = null; try { KeyStore trustStore = KeyStore.getInstance(KeyStore .getDefaultType()); trustStore.load(null, null); sf = new MySSLSocketFactory(trustStore); sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); } catch (KeyManagementException e) { e.printStackTrace(); } catch (UnrecoverableKeyException e) { e.printStackTrace(); } catch (KeyStoreException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } HttpParams params = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT); HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT); SchemeRegistry registry = new SchemeRegistry(); registry.register(new Scheme("https", sf, HTTPS_PORT)); // More customization (https / timeouts etc) can go here... ClientConnectionManager cm = new ThreadSafeClientConnManager( params, registry); DefaultHttpClient client = new DefaultHttpClient(cm, params); // Set the default cookie store client.setCookieStore(COOKIE_STORE); return client; } @Override protected HttpResponse execute(final HttpClient client, final HttpUriRequest request) throws IOException { // Set the http context's cookie storage BasicHttpContext mHttpContext = new BasicHttpContext(); mHttpContext.setAttribute(ClientContext.COOKIE_STORE, COOKIE_STORE); return client.execute(request, mHttpContext); } @Override public Response execute(final Request request) throws IOException { Response response = super.execute(request); if (response.getStatus() == 401) { // Retrofit Callback to handle AccessToken Callback<AccessTockenResponse> accessTokenCallback = new Callback<AccessTockenResponse>() { @SuppressWarnings("deprecation") @Override public void success( AccessTockenResponse loginEntityResponse, Response response) { try { String accessToken = loginEntityResponse .getAccessToken(); TypedOutput body = request.getBody(); ByteArrayOutputStream byte1 = new ByteArrayOutputStream(); body.writeTo(byte1); String s = byte1.toString(); FormUrlEncodedTypedOutput output = new FormUrlEncodedTypedOutput(); String[] pairs = s.split("&"); for (String pair : pairs) { int idx = pair.indexOf("="); if (URLDecoder.decode(pair.substring(0, idx)) .equals("access_token")) { output.addField("access_token", accessToken); } else { output.addField(URLDecoder.decode( pair.substring(0, idx), "UTF-8"), URLDecoder.decode( pair.substring(idx + 1), "UTF-8")); } } execute(new Request(request.getMethod(), request.getUrl(), request.getHeaders(), output)); } catch (IOException e) { e.printStackTrace(); } } @Override public void failure(RetrofitError error) { // Handle Error while refreshing access_token } }; // Call Your retrofit method to refresh ACCESS_TOKEN refreshAccessToken(GRANT_REFRESH,CLIENT_ID, CLIENT_SECRET_KEY,accessToken, accessTokenCallback); } return response; } } 
FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.