web-dev-qa-db-fra.com

Android Retrofit2 Refresh Oauth 2 Token)

J'utilise les bibliothèques Retrofit et OkHttp. J'ai donc Authenticator qui autorise l'utilisateur s'il obtient une réponse 401.

Ma build.gradle est comme ça:

compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
compile 'com.squareup.okhttp3:okhttp:3.1.2'

Et mon Authenticator personnalisé est ici:

import Java.io.IOException;
import okhttp3.Authenticator;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;

public class CustomAuthanticator  implements Authenticator {
@Override
public Request authenticate(Route route, Response response) throws IOException {

    //refresh access token via refreshtoken

    Retrofit client = new Retrofit.Builder()
            .baseUrl(baseurl)
            .addConverterFactory(GsonConverterFactory.create())
            .build();
    APIService service = client.create(APIService.class);
    Call<RefreshTokenResult> refreshTokenResult=service.refreshUserToken("application/json", "application/json", "refresh_token",client_id,client_secret,refresh_token);
    //this is syncronous retrofit request
    RefreshTokenResult refreshResult= refreshTokenResult.execute().body();
    //check if response equals 400 , mean empty response
    if(refreshResult!=null) {
       //save new access and refresh token
        // than create a new request and modify it accordingly using the new token
        return response.request().newBuilder()
                .header("Authorization", newaccesstoken)
                .build();

    } else {
        //we got empty response and return null
        //if we dont return null this method is trying to make so many request
        //to get new access token
        return null;

    }

}}

Voici ma classe APIService:

import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.Query;


public interface APIService {


@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST("token")
public Call<RefreshTokenResult> refreshUserToken(@Header("Accept") String accept, 
    @Header("Content-Type") String contentType, @Field("grant_type") String grantType,
    @Field("client_id") String clientId, @Field("client_secret") String clientSecret, 
    @Field("refresh_token") String refreshToken);
}

J'utilise un authanticateur comme ça:

CustomAuthanticator customAuthanticator=new CustomAuthanticator();
OkHttpClient okClient = new OkHttpClient.Builder()
        .authenticator(customAuthanticator)
        .build();
Gson gson = new GsonBuilder()
        .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
        .create();
Retrofit client = new Retrofit.Builder()
        .baseUrl(getResources().getString(R.string.base_api_url))
        .addConverterFactory(GsonConverterFactory.create(gson))
        .client(okClient)
        .build();

//then make retrofit request

Donc ma question est: Parfois, j'obtiens un nouveau jeton d'accès et continue de travailler, en faisant de nouvelles demandes. Mais parfois, j'obtiens 400 réponses, ce qui signifie une réponse vide. Donc, mon ancien jeton d'actualisation n'est pas valide et je ne peux pas obtenir de nouveau jeton. Normalement, notre jeton d'actualisation expire dans 1 an. Alors comment je peux faire ça. Aidez-moi, s'il vous plaît !

25
Yasin Kaçmaz

Avis de non-responsabilité : En fait, j'utilise Dagger + RxJava + RxAndroid + Retrofit mais je voulais juste apporter une réponse pour démontrer la logique aux futurs visiteurs. La seule différence est d'utiliser Schedulers.trampoline() lors de l'actualisation de votre jeton pour bloquer ce thread. Si vous avez plus de questions sur ces bibliothèques, veuillez commenter ci-dessous afin que je puisse vous fournir une autre réponse ou vous aider.

Également

Important, veuillez lire ceci : Si vous effectuez des requêtes simultanément mais que vous utilisez également dispatcher.setMaxRequests(1); votre token sera actualisé plusieurs fois dans TokenInterceptor classe. Par exemple, lorsque votre application et votre service font des demandes simultanément. Pour contourner ce problème, ajoutez simplement le mot clé synchronized à votre méthode intercept dans TokenInterceptor: public synchronized Response intercept(Chain chain)

enter image description here

@ Edit 07.04.2017:

J'ai mis à jour cette réponse car elle était un peu ancienne et ma situation a changé -maintenant j'ai un service en arrière-plan qui fait aussi des demandes-

Tout d'abord, le processus d'actualisation du jeton est un processus critique. Dans mon application et dans la plupart des applications, procédez comme suit: Si le jeton d'actualisation échoue, déconnectez l'utilisateur actuel et avertissez l'utilisateur de se connecter (vous pouvez peut-être réessayer le processus d'actualisation du jeton de 2-3 à 4 fois selon vous).

@ Avis important : veuillez effectuer des requêtes synchrones lors de l'actualisation de votre jeton dans Authenticator ou Interceptor car vous devez bloquer ce thread jusqu'à ce que votre demande se termine, sinon vos demandes exécutées deux fois avec l'ancien et le nouveau jeton.

Quoi qu'il en soit, je vais l'expliquer étape par étape:

Étape 1: Veuillez vous référer à modèle singleton , nous créerons une classe qui sera responsable de renvoyer notre instance de retrofit à tout moment, où nous voulons accéder. Étant donné que son statique s'il n'y a pas d'instance disponible, il ne crée une instance qu'une seule fois et lorsque vous l'appelez, il renvoie toujours cette instance statique. Il s'agit également d'une définition de base du modèle de conception Singleton.

public class RetrofitClient {

private static Retrofit retrofit = null;

private RetrofitClient() {
    // this default constructor is private and you can't call it like :
    // RetrofitClient client = new RetrofitClient();
    // only way to get it : Retrofit client = RetrofitClient.getInstance();
}

public static Retrofit getInstance() {
    if (retrofit == null) {
        // my token authenticator, I will add this class at below
        TokenAuthenticator tokenAuthenticator = new TokenAuthenticator();

        // I am also using interceptor which controls token if expired
        // lets look at this scenario : My token needs to refresh after 10 hours
        // but I came to application after 50 hours and tried to make request.
        // of course my token is invalid and it will return 401
        // so this interceptor checks time and refreshes token immediately before making request 
        // then continues request with refreshed token
        // So I do not get any 401 response. But if this fails and I get 401 then my TokenAuthenticator do his job.
        // if my TokenAuthenticator fails too, basically I just logout user and tell him to re-login.
        TokenInterceptor tokenInterceptor = new TokenInterceptor();

        // this is the critical point that helped me a lot.
        // we using only one retrofit instance in our application
        // and it uses this dispatcher which can only do 1 request at the same time

        // the docs says : Set the maximum number of requests to execute concurrently.
        // Above this requests queue in memory, waiting for the running calls to complete.

        Dispatcher dispatcher = new Dispatcher();
        dispatcher.setMaxRequests(1);

        // we are using this OkHttp as client, you can add authenticator, interceptors, dispatchers,
        // logging etc. easily for all your requests just editing this OkHttp client
        OkHttpClient okClient = new OkHttpClient.Builder()
                .connectTimeout(Constants.CONNECT_TIMEOUT, TimeUnit.SECONDS)
                .readTimeout(Constants.READ_TIMEOUT, TimeUnit.SECONDS)
                .writeTimeout(Constants.WRITE_TIMEOUT, TimeUnit.SECONDS)
                .authenticator(tokenAuthenticator)
                .addInterceptor(tokenInterceptor)
                .dispatcher(dispatcher)
                .build();

        retrofit = new Retrofit.Builder()
                .baseUrl(context.getResources().getString(R.string.base_api_url))
                .addConverterFactory(GsonConverterFactory.create(new Gson()))
                .client(okClient)
                .build();
    }
    return retrofit;
}

}

Étape 2: Dans la méthode authenticate de mon TokenAuthenticator:

@Override
public Request authenticate(Route route, Response response) throws IOException {
    String userRefreshToken="your refresh token";
    String cid="your client id";
    String csecret="your client secret";
    String baseUrl="your base url";

    refreshResult=refreshToken(baseUrl,userRefreshToken,cid,csecret);
    if (refreshResult) {
    //refresh is successful
    String newaccess="your new access token";

    // make current request with new access token
    return response.request().newBuilder()
            .header("Authorization", newaccess)
            .build();

    } else {
        // refresh failed , maybe you can logout user
        // returning null is critical here, because if you do not return null 
        // it will try to refresh token continuously like 1000 times.
        // also you can try 2-3-4 times by depending you before logging out your user
        return null;
    }
}

et la méthode refreshToken, c'est juste un exemple où vous pouvez créer votre propre stratégie lors de l'actualisation de votre token. J'utilise HttpUrlConnection parce que j'ai des circonstances supplémentaires lors de l'actualisation de mon jeton. En attendant, je vous encourage à utiliser Retrofit. Quoi qu'il en soit:

public boolean refreshToken(String url,String refresh,String cid,String csecret) throws IOException{
    URL refreshUrl=new URL(url+"token");
    HttpURLConnection urlConnection = (HttpURLConnection) refreshUrl.openConnection();
    urlConnection.setDoInput(true);
    urlConnection.setRequestMethod("POST");
    urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
    urlConnection.setUseCaches(false);
    String urlParameters  = "grant_type=refresh_token&client_id="+cid+"&client_secret="+csecret+"&refresh_token="+refresh;

    urlConnection.setDoOutput(true);
    DataOutputStream wr = new DataOutputStream(urlConnection.getOutputStream());
    wr.writeBytes(urlParameters);
    wr.flush();
    wr.close();

    int responseCode = urlConnection.getResponseCode();

    if(responseCode==200){
        BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
        String inputLine;
        StringBuffer response = new StringBuffer();

        while ((inputLine = in.readLine()) != null) {
            response.append(inputLine);
        }
        in.close();

        // this gson part is optional , you can read response directly from Json too
        Gson gson = new Gson();
        RefreshTokenResult refreshTokenResult=gson.fromJson(response.toString(),RefreshTokenResult.class);

        // handle new token ...
        // save it to the sharedpreferences, storage bla bla ...
        return true;

    } else {
        //cannot refresh
        return false;
    } 

}

Étape 3: En fait, nous l'avons fait mais je vais montrer une utilisation simple:

Retrofit client= RetrofitClient.getInstance();
//interface for requests
APIService service = client.create(APIService.class);
// then do your requests .....

Étape 4: Pour ceux qui veulent voir la logique TokenInterceptor:

public class TokenInterceptor implements Interceptor{
Context ctx;
SharedPreferences mPrefs;
SharedPreferences.Editor mPrefsEdit;

public TokenInterceptor(Context ctx) {
    this.ctx = ctx;
    this.mPrefs= PreferenceManager.getDefaultSharedPreferences(ctx);
    mPrefsEdit=mPrefs.edit();
}

@Override
public synchronized Response intercept(Chain chain) throws IOException {

    Request newRequest=chain.request();

    //when saving expire time :
    integer expiresIn=response.getExpiresIn();
    Calendar c = Calendar.getInstance();
    c.add(Calendar.SECOND,expiresIn);
    mPrefsEdit.putLong("expiretime",c.getTimeInMillis());

    //get expire time from shared preferences
    long expireTime=mPrefs.getLong("expiretime",0);
    Calendar c = Calendar.getInstance();
    Date nowDate=c.getTime();
    c.setTimeInMillis(expireTime);
    Date expireDate=c.getTime();

    int result=nowDate.compareTo(expireDate);
    /**
     * when comparing dates -1 means date passed so we need to refresh token
     * see {@link Date#compareTo}
     */
    if(result==-1) {
        //refresh token here , and got new access token
        String newaccessToken="newaccess";
        newRequest=chain.request().newBuilder()
                .header("Authorization", newaccessToken)
                .build();
    }
    return chain.proceed(newRequest);
  }
}

Dans ma demande, je fais des demandes au service d'application et d'arrière-plan. Les deux utilisant la même instance et je peux facilement gérer. Veuillez vous référer à cette réponse et essayez de créer votre propre client. Si vous avez encore des problèmes, faites simplement un commentaire ci-dessous, mentionnez-moi, même une autre question, ou envoyez un courrier. J'aiderai quand j'aurai le temps. J'espère que cela t'aides.

33
Yasin Kaçmaz

Dans votre classe ApiClient.Java:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new AuthorizationInterceptor(context))
                .build();

Ajoutez TokenManager.Java classe dans votre package de mise à niveau

package co.abc.retrofit;

/**
 * Created by ravindrashekhawat on 17/03/17.
 */

public interface TokenManager {
    String getToken();
    boolean hasToken();
    void clearToken();
    String refreshToken();
}

Ajoutez la classe Intercepter dans votre package avec le nom AuthorizationInterceptor.Java

package co.smsmagic.retrofit;

import Android.content.Context;
import Android.content.SharedPreferences;
import Android.preference.PreferenceManager;
import Android.util.Log;

import com.google.gson.Gson;

import org.json.JSONException;
import org.json.JSONObject;

import Java.io.IOException;

import co.abc.models.RefreshTokenResponseModel;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Retrofit;
import retrofit2.http.Header;

import static co.abc.utils.abcConstants.ACCESS_TOKEN;
import static co.abc.utils.abcConstants.BASE_URL;
import static co.abc.utils.abcConstants.GCM_TOKEN;
import static co.abc.utils.abcConstants.JWT_TOKEN_PREFIX;
import static co.abc.utils.abcConstants.REFRESH_TOKEN;

/**
 * Created by ravindrashekhawat on 21/03/17.
 */

public class AuthorizationInterceptor implements Interceptor {
    private static Retrofit retrofit = null;
    private static String deviceToken;
    private static String accessToken;
    private static String refreshToken;
    private static TokenManager tokenManager;
    private static Context mContext;

    public AuthorizationInterceptor(Context context) {
        this.mContext = context;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Request modifiedRequest = null;

        tokenManager = new TokenManager() {
            final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);

            @Override
            public String getToken() {

                accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
                return accessToken;
            }

            @Override
            public boolean hasToken() {
                accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
                if (accessToken != null && !accessToken.equals("")) {
                    return true;
                }
                return false;
            }

            @Override
            public void clearToken() {
                sharedPreferences.edit().putString(ACCESS_TOKEN, "").apply();
            }

            @Override
            public String refreshToken() {
                final String accessToken = null;

                RequestBody reqbody = RequestBody.create(null, new byte[0]);
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                        .url(BASE_URL + "refresh")
                        .method("POST", reqbody)
                        .addHeader("Authorization", JWT_TOKEN_PREFIX + refreshToken)
                        .build();

                try {
                    Response response = client.newCall(request).execute();
                    if ((response.code()) == 200) {
                        // Get response
                        String jsonData = response.body().string();

                        Gson gson = new Gson();
                        RefreshTokenResponseModel refreshTokenResponseModel = gson.fromJson(jsonData, RefreshTokenResponseModel.class);
                        if (refreshTokenResponseModel.getRespCode().equals("1")) {
                            sharedPreferences.edit().putString(ACCESS_TOKEN, refreshTokenResponseModel.getResponse()).apply();
                            return refreshTokenResponseModel.getResponse();
                        }

                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return accessToken;
            }
        };

        final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
        deviceToken = sharedPreferences.getString(GCM_TOKEN, "");
        accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
        refreshToken = sharedPreferences.getString(REFRESH_TOKEN, "");

        Response response = chain.proceed(request);
        boolean unauthorized =false;
        if(response.code() == 401 || response.code() == 422){
            unauthorized=true;
        }

        if (unauthorized) {
            tokenManager.clearToken();
            tokenManager.refreshToken();
            accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
            if(accessToken!=null){
                modifiedRequest = request.newBuilder()
                        .addHeader("Authorization", JWT_TOKEN_PREFIX + tokenManager.getToken())
                        .build();
                return chain.proceed(modifiedRequest);
            }
        }
        return response;
    }
}

Note: Ceci est un code de travail pour le jeton de rafraîchissement que j'ai fourni, restez calme, juste pour changer une constante, sauf que cela fonctionnera parfaitement. Essayez simplement de comprendre la logique.

En bas il y a une logique pour rappeler la même requête

 if(accessToken!=null){
                modifiedRequest = request.newBuilder()
                        .addHeader("Authorization", JWT_TOKEN_PREFIX + tokenManager.getToken())
                        .build();
                return chain.proceed(modifiedRequest);
  }
3
Ravindra Shekhawat