Actualizar el token de OAuth con Retrofit sin modificar todas las llamadas

Estamos utilizando Retrofit en nuestra aplicación de Android para comunicarnos con un servidor seguro de OAuth2. Todo funciona muy bien, usamos RequestInterceptor para incluir el token de acceso con cada llamada. Sin embargo, habrá momentos en los que el token de acceso caducará y el token deberá actualizarse. Cuando el token caduque, la próxima llamada volverá con un código HTTP no autorizado, por lo que es fácil de controlar. Podríamos modificar cada llamada de Retrofit de la siguiente manera: En la callback fallida, verifique el código de error, si es No autorizado, actualice el token de OAuth y luego repita la llamada de Retrofit. Sin embargo, para esto, todas las llamadas deberían modificarse, lo cual no es una solución fácil de mantener y buena. ¿Hay alguna forma de hacerlo sin modificar todas las llamadas de Retrofit?

Por favor, no use Interceptors para tratar con la autenticación.

Actualmente, el mejor enfoque para manejar la autenticación es usar la nueva API Authenticator , diseñada específicamente para este propósito .

OkHttp le pedirá automáticamente al Authenticator credenciales cuando la respuesta sea 401 Not Authorised reintentando la última solicitud fallida con ellas.

 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 los Interceptors

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

Utilice este cliente al crear su RestAdapter

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

Si está utilizando Retrofit > = 1.9.0 , puede utilizar el nuevo Interceptor de OkHttp , que se introdujo en OkHttp 2.2.0 . Desearía utilizar un Interceptor de aplicación , que le permite retry and make multiple calls a retry and make multiple calls .

Su interceptor podría parecerse 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, use este OkHttpClient al crear su RestAdapter .

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

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

Habiendo dicho eso, 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 grupos de conexiones ..

Si tiene, por ejemplo, un Retrofit TokenService que necesita dentro de su Authenticator pero solo desea configurar un OkHttpClient , puede usar un TokenServiceHolder como una dependencia para TokenAuthenticator . Debería mantener una referencia en el nivel de aplicación (singleton). Esto es fácil si está usando Dagger 2; de lo contrario, simplemente cree un 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á utilizando Dagger 2 o un marco de dependency injection similar, hay algunos ejemplos en las respuestas a esta pregunta

Puede intentar crear una clase base para todos sus cargadores en la que pueda detectar una excepción en particular y actuar según lo necesite. Haga que todos los cargadores diferentes se extiendan desde la clase base para distribuir el comportamiento.

Después de una larga investigación, personalicé el cliente Apache para manejar Refreshing AccessToken For Retrofit En el que envía 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 Persistent Client que mantiene cookies para todas las solicitudes y verificaciones con cada respuesta de solicitud, si es un acceso no autorizado ERROR_CODE = 401, actualiza el token de acceso y recupera la solicitud, de lo contrario solo 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 accessTokenCallback = new Callback() { @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; } } 

Para cualquier persona que desee resolver llamadas concurrentes / paralelas al actualizar el token. Aquí hay una solución

 class TokenAuthenticator: Authenticator { override fun authenticate(route: Route?, response: Response?): Request? { response?.let { if (response.code() == 401) { while (true) { if (!isRefreshing) { val requestToken = response.request().header(AuthorisationInterceptor.AUTHORISATION) val currentToken = OkHttpUtil.headerBuilder(UserService.instance.token) currentToken?.let { if (requestToken != currentToken) { return generateRequest(response, currentToken) } } val token = refreshToken() token?.let { return generateRequest(response, token) } } } } } return null } private fun generateRequest(response: Response, token: String): Request? { return response.request().newBuilder() .header(AuthorisationInterceptor.USER_AGENT, OkHttpUtil.UA) .header(AuthorisationInterceptor.AUTHORISATION, token) .build() } private fun refreshToken(): String? { synchronized(TokenAuthenticator::class.java) { UserService.instance.token?.let { isRefreshing = true val call = ApiHelper.refreshToken() val token = call.execute().body() UserService.instance.setToken(token, false) isRefreshing = false return OkHttpUtil.headerBuilder(token) } } return null } companion object { var isRefreshing = false } } 

Sé que este es un hilo viejo, pero por si acaso alguien se tropezó con él.

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 grupos de conexiones ..

Estaba enfrentando el mismo problema, pero quería crear solo una OkHttpClient porque no creo que necesite otra para el TokenAuthenticator en sí, estaba usando Dagger2, así que terminé proporcionando la clase de servicio como Lazy inyectado en el TokenAuthenticator, puedes leer más acerca de Lazy injection in dagger 2 aquí , pero es básicamente como decirle a Dagger que NO vaya y cree el servicio que necesita el TokenAuthenticator de inmediato.

Puede hacer referencia a este subproceso SO para el código de muestra: ¿Cómo resolver una dependencia circular mientras sigue usando Dagger2?

Usar TokenAuthenticator como @theblang answer es una forma correcta de manejar refresh_token .

Aquí está mi implemento (tengo el uso de Kotlin, Dagger, RX pero puede usar esta idea para implementarlo en su caso)
TokenAuthenticator

 class TokenAuthenticator @Inject constructor(private val noneAuthAPI: PotoNoneAuthApi, private val accessTokenWrapper: AccessTokenWrapper) : Authenticator { override fun authenticate(route: Route, response: Response): Request? { val newAccessToken = noneAuthAPI.refreshToken(accessTokenWrapper.getAccessToken()!!.refreshToken).blockingGet() accessTokenWrapper.saveAccessToken(newAccessToken) // save new access_token for next called return response.request().newBuilder() .header("Authorization", newAccessToken.token) // just only need to override "Authorization" header, don't need to override all header since this new request is create base on old request .build() } } 

Para evitar un ciclo de dependencia como el comentario de @Brais Gabin, creo 2 interfaces como

 interface PotoNoneAuthApi { // NONE authentication API @POST("/login") fun login(@Body request: LoginRequest): Single @POST("refresh_token") @FormUrlEncoded fun refreshToken(@Field("refresh_token") refreshToken: String): Single } 

y

 interface PotoAuthApi { // Authentication API @GET("api/images") fun getImage(): Single } 

Clase AccessTokenWrapper

 class AccessTokenWrapper constructor(private val sharedPrefApi: SharedPrefApi) { private var accessToken: AccessToken? = null // get accessToken from cache or from SharePreference fun getAccessToken(): AccessToken? { if (accessToken == null) { accessToken = sharedPrefApi.getObject(SharedPrefApi.ACCESS_TOKEN, AccessToken::class.java) } return accessToken } // save accessToken to SharePreference fun saveAccessToken(accessToken: AccessToken) { this.accessToken = accessToken sharedPrefApi.putObject(SharedPrefApi.ACCESS_TOKEN, accessToken) } } 

Clase AccessToken

 data class AccessToken( @Expose var token: String, @Expose var refreshToken: String) 

Mi interceptor

 class AuthInterceptor @Inject constructor(private val accessTokenWrapper: AccessTokenWrapper): Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() val authorisedRequestBuilder = originalRequest.newBuilder() .addHeader("Authorization", accessTokenWrapper.getAccessToken()!!.token) .header("Accept", "application/json") return chain.proceed(authorisedRequestBuilder.build()) } } 

Finalmente, agregue Interceptor y Authenticator a su OKHttpClient cuando cree el servicio PotoAuthApi

Manifestación

https://github.com/PhanVanLinh/AndroidMVPKotlin

Nota

Flujo de autenticador

  • Ejemplo de API getImage() return 401 código de error
  • authenticate método dentro de TokenAuthenticator disparará
  • Sincronizar noneAuthAPI.refreshToken(...) llamado
  • Después de la noneAuthAPI.refreshToken(...) -> token nuevo se agregará al encabezado
  • getImage() llamará AUTO con el nuevo encabezado ( HttpLogging NO registrará esta llamada) ( intercept dentro de AuthInterceptor NO SE AuthInterceptor )
  • Si getImage() todavía falla con el error 401, el método de authenticate dentro de TokenAuthenticator se TokenAuthenticator OTRA VEZ y TokenAuthenticator arrojará error sobre el método de llamada muchas veces. Puede prevenirlo por conteo de respuesta . Ejemplo, si return null en la authenticate después de 3 getImage() rebash, getImage() finalizará y return response 401

  • Si getImage() response success => daremos el resultado normalmente (como llama a getImage() sin error)

Espero que ayude