web-dev-qa-db-ja.com

Android Retrofit2 Refresh Oauth 2トークン

RetrofitおよびOkHttpライブラリを使用しています。したがって、401 [応答]を取得した場合にユーザーを認証するAuthenticatorがあります。

僕の 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);
}

私はそのようなオーセンティケーターを使用しています:

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年です。だから私はこれをどのように行うことができます。私を助けてください !

25
Yasin Kaçmaz

免責事項:実際に使用しているのはDagger + RxJava + RxAndroid + Retrofitですが、単に答えを提供したかっただけです将来の訪問者のためのロジックを示します。唯一の違いは、トークンを更新してそのスレッドをブロックするときにSchedulers.trampoline()を使用することです。これらのライブラリについてさらに質問がある場合は、以下にコメントしてください。別の回答を提供したり、サポートしたりできます。

また

重要なことをお読みください:同時にリクエストを行っているときにdispatcher.setMaxRequests(1);を使用している場合、TokenInterceptorクラス内でトークンが複数回更新されます。たとえば、アプリとサービスが同時にリクエストを行う場合。この問題を克服するには、synchronizedキーワードをintercept内のTokenInterceptorメソッドに追加するだけです:public synchronized Response intercept(Chain chain)

enter image description here

@ Edit 07.04.2017:

少し古くて状況が変わったため、この回答を更新しました-今ではリクエストも行うバックグラウンドサービスがあります-

まず、更新トークンプロセスは重要なプロセスです。私のアプリケーションとこれを行うほとんどのアプリケーションで:リフレッシュトークンが現在のユーザーのログアウトに失敗し、ユーザーにログインを警告する場合(たぶん、あなたに応じて2-3-4回リフレッシュトークンプロセスを再試行できます)

@ Important NoticeAuthenticatorまたはInterceptor内でトークンを更新するときは、リクエストを完了するまでスレッドをブロックする必要があるため、同期リクエストを作成してください。古いトークンと新しいトークンで2回実行されます。

とにかく、私はそれを段階的に説明します:

ステップ1:を参照してください シングルトンパターン 、アクセスしたいときはいつでもレトロフィットインスタンスを返す1つのクラスを作成します。使用可能なインスタンスがない場合は静的なので、インスタンスを作成するのは一度だけであり、呼び出すと常にこの静的インスタンスが返されます。これは、シングルトン設計パターンの基本的な定義でもあります。

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

}

ステップ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 method、これはトークンを更新するときに独自の戦略を作成できる単なる例です。トークンを更新するときに余分な状況があるため、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);
  }
}

私のアプリケーションでは、アプリケーションおよびバックグラウンドサービスでリクエストを行っています。どちらも同じインスタンスを使用しており、簡単に管理できます。この回答を参照して、独自のクライアントを作成してください。それでも問題が解決しない場合は、コメントを追加してください。別の質問でもかまいませんか、メールを送信してください。時間があるときにお手伝いします。お役に立てれば。

33
Yasin Kaçmaz

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