2016-02-19 229 views
19

我正在使用RetrofitOkHttp庫。 所以我有Authenticator哪些authanticate用戶如果得到401響應。Android Retrofit2刷新Oauth 2令牌

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年內到期。所以我該如何做到這一點。請幫幫我 !

回答

20

免責聲明: 其實我使用Dagger + RxJava + RxAndroid + Retrofit,但我只是想提供一個答案,以展示對未來遊客的邏輯。 刷新令牌以阻止該線程時,唯一區別是使用Schedulers.trampoline()。如果您對這些圖書館有更多疑問,請在下面評論,以便我可以提供其他答案或幫助您。

而且

重要請仔細閱讀本: 如果你同時發出請求,而且使用dispatcher.setMaxRequests(1);您的令牌將刷新內部TokenInterceptor類多次。例如,當您的應用和您的服務同時發出請求時。爲了擊敗這個問題只需要添加​​關鍵字到intercept方法內TokenInterceptorpublic synchronized Response intercept(Chain chain)

enter image description here

@Edit 2017年4月7日:

我這個答案進行更新,因爲這是一個有點老我的情況改變了 - 現在我有一個後臺服務,它也提出了請求 -

首先是refre sh令牌進程是關鍵進程。在我的應用程序和大多數應用程序中這樣做:如果刷新令牌未能註銷當前用戶並警告用戶登錄。(也許你可以根據你2-3-4時間重試刷新令牌過程)

@Important注意:清爽內AuthenticatorInterceptor你的令牌時請同步請求,因爲你必須阻止線程,直到你請求完成否則您的請求執行兩次與舊的和新的令牌。

反正我會解釋它一步一步:

第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 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方法,這僅僅是例子,你可以刷新你的令牌時創建自己的策略。我正在使用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); 
    } 
} 

在我的應用程序中,我在應用程序和後臺服務中提出請求。他們都使用相同的實例,我可以輕鬆管理。請參考這個答案,並嘗試創建自己的客戶端。如果您仍然有問題,請在下面評論,請提及我 - 甚至是另一個問題 - 或發送郵件。有空的時候我會幫忙的。希望這可以幫助。

+1

很好的回答。我在哪裏可以找到'TokenInterceptor'類? –

+0

親愛的@IbrahimDisouki我爲TokenInterceptor邏輯添加了一個步驟。請結帳,讓我知道你是否有問題。 –

+0

感謝您的回覆。 'integer expiresIn = response.getExpiresIn();'響應對象來自哪裏? –

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

在你的包添加截擊類名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); 
    } 
+0

Thats 401刷新令牌失敗不是400.But我已經使用TokenManage r界面不說任何錯誤的方法也只是總有很多方法來實現相同的解決方案。可以幫助某人 –

+0

是的,你的方法是好的。但是您必須嘗試執行您的請求以查看它是否返回任何401響應(未授權)。您可以將此邏輯移動到OkHttp的身份驗證器,因爲它執行同樣的操作,您可以在另一個攔截器中檢查令牌時間。如果時間過期,基本上你可以刷新你的令牌而不需要401響應。 –