Android Retrofit2 Обновить Oauth 2 Token

Я использую библиотеки Retrofit и OkHttp. Поэтому у меня есть Authenticator, который аутентифицирует пользователя, если получает ответ 401.

Мой build.gradle выглядит следующим образом:

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'

И мой пользовательский Authenticator находится здесь:

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;

    }

}}

Это мой класс 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);
}

Я использую authanticator следующим образом:

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

Итак, мой вопрос: Иногда я получаю новый токен доступа и продолжаю работать, создавая новые запросы. Но иногда я получаю 400 ответов, что означает пустой ответ. Поэтому мой старый токен обновления недействителен, и я не могу получить новый токен. Обычно токен обновления обновляется через 1 год. Так как я могу это сделать. Пожалуйста, помогите мне!

Ответы

Ответ 1

Отказ от ответственности: На самом деле я использую Dagger + RxJava + RxAndroid + Retrofit, но я просто хотел дать ответ, чтобы продемонстрировать логику для будущих посетителей. Единственное отличие заключается в использовании Schedulers.trampoline() при обновлении вашего токена для блокировки этого потока. Если у вас есть дополнительные вопросы об этих библиотеках, прокомментируйте это ниже, поэтому, возможно, я смогу предоставить вам другой ответ или помочь вам.

Кроме

Важно Пожалуйста, прочтите это: Если вы делаете запросы одновременно, но также используя dispatcher.setMaxRequests(1);, ваш токен будет обновляться несколько раз внутри класса TokenInterceptor. Например, приложение делает запрос, но в то же время ваша служба также запрашивает запрос. Чтобы избить эту проблему, просто добавьте ключевое слово synchronized к вашему методу intercept внутри TokenInterceptor: public **synchronized** Response intercept(Chain chain)

введите описание изображения здесь

@Edit 07.04.2017:

Я обновил этот ответ, потому что он был немного старым, и мои обстоятельства изменились - теперь у меня есть фоновый сервис, который также вызывает запросы -

Прежде всего, процесс токен обновления является критическим процессом. В моем приложении и большинстве приложений это делает: Если обновить токен не удается выйти из текущего пользователя и предупредить пользователя о входе в систему. (Возможно, вы можете повторить попытку обновления токена в 2-3-4 раза в зависимости от вас)

Замечание @Important. Пожалуйста, делайте синхронные запросы при обновлении вашего токена внутри Authenticator или Interceptor из-за того, что вы должны блокировать этот поток до тех пор, пока ваш запрос не завершится иначе, ваши запросы будут выполняться дважды со старыми и новыми лексема.

В любом случае я объясню это шаг за шагом:

Шаг 1: Обратитесь singleton pattern, мы создадим один класс, ответственный за возврат нашего экземпляра модификации где бы мы ни находились. Поскольку его статический, если нет экземпляра, он просто создает экземпляр только один раз, и когда вы вызываете его, он всегда возвращает этот статический экземпляр. Это также базовое определение шаблона проектирования 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 calling it : Retrofit client = RetrofitClient.getInstance();
}

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

        // I am also using interceptor which controls token if expired
        // lets look at this scenerio : if my token needs to refresh after 10 hours but I came
        // to application after 50 hours and tried to make request.
        // ofc my token is invalid and if I make request it will return 401
        // so this interceptor checks time and refresh token immediately before making request and after makes current request
        // with refreshed token. So I do not get 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 relogin.
        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 using this OkHttp, you can add authenticator, interceptors, dispatchers,
        // logging stuff etc. easily for all your requests just editing this OkHttp
        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;
}

}

Шаг 2: В моем методе TokenAuthenticator authenticate:

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

и refreshToken, это просто пример, который вы можете создать свою собственную стратегию при обновлении вашего токена. Я использую HttpUrlConnection, потому что у меня есть дополнительные обстоятельства при обновлении моего токена. Тем временем я рекомендую вам использовать Retrofit. В любом случае:

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

}

Шаг 3: На самом деле мы сделали это, но я покажу простое использование:

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

Шаг 4: Для тех, кто хочет видеть логику 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);
  }
}

В моем приложении я делаю запросы в приложении и в фоновом режиме. Оба они используют один и тот же экземпляр, и я могу легко справиться. Пожалуйста, обратитесь к этому ответу и попробуйте создать своего собственного клиента. Если у вас все еще есть вопросы, просто прокомментируйте ниже, укажите мне - еще один вопрос - или отправьте письмо. Я помогу, когда у меня будет время. Надеюсь, это поможет.

Ответ 2

В классе ApiClient.java:

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

Добавить класс TokenManager.java в пакет обновления

package co.abc.retrofit;

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

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

Добавьте класс Intercepter в свой пакет с именем 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;
    }
}

Примечание: Этот рабочий код для токена обновления, который я предоставил, оставил вас спокойным, чтобы изменить константу, за исключением того, что она будет работать отлично. Просто попробуйте понять логику.

Внизу есть логика для повторного вызова того же запроса

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