web-dev-qa-db-fra.com

Rafraîchissement OAuth utilisant Retrofit sans modifier tous les appels

Nous utilisons Retrofit dans notre Android application) pour communiquer avec un serveur sécurisé OAuth2. Tout fonctionne parfaitement, nous utilisons le RequestInterceptor pour inclure le jeton d'accès à chaque appel. Cependant, il y aura des moments où le jeton d'accès expirera et le jeton devra être actualisé. Lorsque le jeton expirera, le prochain appel sera renvoyé avec un code HTTP non autorisé, ce qui facilitera la surveillance. Nous pourrions modifier chaque appel de modification de la manière suivante: Dans le rappel d'échec , recherchez le code d'erreur; s'il est égal à Non autorisé, actualisez le jeton OAuth, puis répétez l'appel de modification. Toutefois, pour cela, tous les appels doivent être modifiés, ce qui n'est pas facilement gérable, et bonne solution: existe-t-il un moyen de le faire sans modifier tous les appels de modification?

138
Daniel Zolnai

Merci de ne pas utiliser Interceptors pour gérer l'authentification.

Actuellement, la meilleure approche pour gérer l'authentification consiste à utiliser la nouvelle API Authenticator , conçue spécifiquement pour cette fin .

OkHttp va demander automatiquement le Authenticator pour obtenir les informations d'identification lorsqu'une réponse est 401 Not Authorised réessaie avec la dernière demande échouée .

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;
    }

Attachez un Authenticator à un OkHttpClient comme vous le feriez avec Interceptors

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

Utilisez ce client lors de la création de votre RetrofitRestAdapter

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

Si vous utilisez Retrofit > = 1.9.0 alors vous pouvez utiliser OkHttp's new Interceptor , qui a été introduit dans OkHttp 2.2.0. Vous voudriez utiliser un Application Interceptor , qui vous permet de retry and make multiple calls.

Votre intercepteur pourrait ressembler à ce pseudocode:

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;
    }

}

Après avoir défini votre Interceptor, créez un OkHttpClient et ajoutez l'intercepteur en tant que Application Interceptor .

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

Et enfin, utilisez ceci OkHttpClient lors de la création de votre RestAdapter.

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

Attention: As Jesse Wilson (de Square) mentionne ici , il s’agit d’une quantité de pouvoir dangereuse.

Cela étant dit, je pense vraiment que c’est la meilleure façon de gérer une situation de ce type maintenant. Si vous avez des questions s'il vous plaît n'hésitez pas à demander dans un commentaire.

62
theblang

TokenAuthenticator dépend d'une classe de service. La classe de service dépend d'une instance OkHttpClient. Pour créer un OkHttpClient, j'ai besoin de TokenAuthenticator. Comment puis-je briser ce cycle? Deux OkHttpClients différents? Ils vont avoir différents pools de connexion.

Si vous possédez, par exemple, un Retrofit TokenService dont vous avez besoin à l'intérieur de votre Authenticator mais que vous ne souhaitez configurer qu'un seul OkHttpClient, vous pouvez utiliser un correcteur TokenServiceHolder en tant que dépendance de TokenAuthenticator. Vous devez conserver une référence au niveau de l'application (singleton). Cela est facile si vous utilisez Dagger 2, sinon créez simplement un champ de classe dans votre application.

Dans 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;
    }

Dans TokenServiceHolder.Java:

public class TokenServiceHolder {

    TokenService tokenService = null;

    @Nullable
    public TokenService get() {
        return tokenService;
    }

    public void set(TokenService tokenService) {
        this.tokenService = tokenService;
    }
}

Configuration du client:

//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 vous utilisez Dagger 2 ou un framework d'injection de dépendance similaire, vous trouverez des exemples dans les réponses à cette question

21
David Rawson

Utiliser TokenAuthenticator comme @theblang answer est un moyen correct pour gérer refresh_token.

Voici mon outil (j'ai déjà utilisé Kotlin, Dagger, RX mais vous pouvez utiliser cette idée pour mettre en œuvre votre cas)
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()
    }
}

Pour empêcher le cycle de dépendance comme @Brais Gabin commenter, je crée une interface 2 comme

interface PotoNoneAuthApi { // NONE authentication API
    @POST("/login")
    fun login(@Body request: LoginRequest): Single<AccessToken>

    @POST("refresh_token")
    @FormUrlEncoded
    fun refreshToken(@Field("refresh_token") refreshToken: String): Single<AccessToken>
}

et

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

AccessTokenWrapper classe

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)
    }
}

AccessToken classe

data class AccessToken(
        @Expose
        var token: String,

        @Expose
        var refreshToken: String)

Mon intercepteur

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())
    }
}

Enfin, ajoutez Interceptor et Authenticator à votre OKHttpClient lors de la création du service PotoAuthApi

Démo

https://github.com/PhanVanLinh/AndroidMVPKotlin

Remarque

  • Exemple d'API getImage() retourne un code d'erreur 401
  • authenticate la méthode à l'intérieur de TokenAuthenticator sera déclenchée
  • Synchroniser noneAuthAPI.refreshToken(...) appelée
  • Après noneAuthAPI.refreshToken(...) réponse -> le nouveau jeton s'ajoutera à l'en-tête
  • getImage() sera AUTO appelé avec un nouvel en-tête (HttpLogging NE SERA PAS connecté cet appel) (intercept à l'intérieur AuthInterceptor NE SERA PAS APPELÉ )
  • Si getImage() échouait toujours avec l'erreur 401, la méthode authenticate à l'intérieur de TokenAuthenticator se déclencherait à nouveau et à nouveau alors il va jeter erreur sur la méthode d'appel plusieurs fois (Java.net.ProtocolException: Too many follow-up requests). Vous pouvez l’empêcher par nombre de réponses . Exemple, si vous return null Dans authenticate après trois tentatives, getImage() finira et return response 401

  • Si getImage() réponse réussie => nous obtiendrons le résultat normalement (comme vous appelez getImage() sans erreur)

J'espère que ça aide

2
Phan Van Linh

Je sais que c'est un vieux fil, mais juste au cas où quelqu'un trébucherait dedans.

TokenAuthenticator dépend d'une classe de service. La classe de service dépend d'une instance OkHttpClient. Pour créer un OkHttpClient, j'ai besoin de TokenAuthenticator. Comment puis-je briser ce cycle? Deux OkHttpClients différents? Ils vont avoir différents pools de connexion.

Je faisais face au même problème, mais je voulais créer un seul OkHttpClient car je ne pensais pas en avoir besoin d’un autre pour TokenAuthenticator lui-même, j’utilisais Dagger2. J’ai donc fourni la classe de service sous la forme Lazy injecté dans TokenAuthenticator, vous pouvez en savoir plus sur Lazy injection in dagger 2 ici , mais c'est comme dire fondamentalement à Dagger de [~ # ~] pas [~ # ~] va immédiatement créer le service requis par TokenAuthenticator.

Vous pouvez vous référer à ce SO thread pour exemple de code: Comment résoudre une dépendance circulaire tout en utilisant toujours Dagger2?

1
Boda

Après de longues recherches, j'ai personnalisé le client Apache pour qu'il gère l'actualisation de AccessToken for Retrofit dans lequel vous envoyez un jeton d'accès en tant que paramètre.

Initiez votre adaptateur avec le client persistant cookie

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

Cookie Client permanent qui conserve les cookies pour toutes les demandes et vérifie chaque réponse à la demande. S'il s'agit d'un accès non autorisé, ERROR_CODE = 401, actualise le jeton d'accès et rappelle la demande, sinon il ne traite que la demande.

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;
    }
}
0
Suneel Prakash

Vous pouvez essayer de créer une classe de base pour tous vos chargeurs dans laquelle vous pourrez intercepter une exception particulière, puis agir selon vos besoins. Faites en sorte que tous vos chargeurs soient étendus à partir de la classe de base afin de répandre le comportement.

0
k3v1n4ud3

Utiliser un intercepteur (injecter le jeton) et un authentificateur (opérations de rafraîchissement) remplit son rôle, mais:

J'ai également eu un problème de double appel: le premier appel a toujours renvoyé un 401: le jeton n'a pas été injecté au premier appel (intercepteur) et l'authentificateur a été appelé: deux demandes ont été effectuées.

Le correctif consistait simplement à réaffecter la demande à la construction de l'intercepteur:

AVANT:

private Interceptor getInterceptor() {
    return (chain) -> {
        Request request = chain.request();
        //...
        request.newBuilder()
                .header(AUTHORIZATION, token))
                .build();
        return chain.proceed(request);
    };
}

APRÈS:

private Interceptor getInterceptor() {
    return (chain) -> {
        Request request = chain.request();
        //...
        request = request.newBuilder()
                .header(AUTHORIZATION, token))
                .build();
        return chain.proceed(request);
    };
}

EN UN BLOC:

private Interceptor getInterceptor() {
    return (chain) -> {
        Request request = chain.request().newBuilder()
                .header(AUTHORIZATION, token))
                .build();
        return chain.proceed(request);
    };
}

J'espère que ça aide.

Edit: Je n'ai pas trouvé le moyen d'éviter le premier appel pour toujours renvoyer le 401 en utilisant uniquement l'authentificateur et aucun intercepteur

0
Sigrun