2015-10-03 20 views
7

我正在使用Retrofit處理與服務器API,API用戶JSON Web令牌進行身份驗證的通信。令牌會不時失效,我正在尋找實現改進客戶端的最佳方法,當客戶端到期時可以自動刷新令牌。爲WebToken驗證改造自定義客戶端

這是首次執行,我想出了,:

/** 
* Client implementation that refreshes JSON WebToken automatically if 
* the response contains a 401 header, has there may be simultaneous calls to execute method 
* the refreshToken is synchronized to avoid multiple login calls. 
*/ 
public class RefreshTokenClient extends OkClient { 


private static final int UNAUTHENTICATED = 401; 


/** 
* Application context 
*/ 
private Application mContext; 



public RefreshTokenClient(OkHttpClient client, Application application) { 
    super(client); 
    mContext = application; 
} 


@Override 
public Response execute(Request request) throws IOException { 

    Timber.d("Execute request: " + request.getMethod() + " - " + request.getUrl()); 

    //Make the request and check for 401 header 
    Response response = super.execute(request); 

    Timber.d("Headers: "+ request.getHeaders()); 

    //If we received a 401 header, and we have a token, it's most likely that 
    //the token we have has expired 
    if(response.getStatus() == UNAUTHENTICATED && hasToken()) { 

     Timber.d("Received 401 from server awaiting"); 

     //Clear the token 
     clearToken(); 

     //Gets a new token 
     refreshToken(request); 

     //Update token in the request 
     Timber.d("Make the call again with the new token"); 

     //Makes the call again 
     return super.execute(rebuildRequest(request)); 

    } 

    return response; 
} 


/** 
* Rebuilds the request to be executed, overrides the headers with the new token 
* @param request 
* @return new request to be made 
*/ 
private Request rebuildRequest(Request request){ 

    List<Header> newHeaders = new ArrayList<>(); 
    for(Header h : request.getHeaders()){ 
     if(!h.getName().equals(Constants.Headers.USER_TOKEN)){ 
      newHeaders.add(h); 
     } 
    } 
    newHeaders.add(new Header(Constants.Headers.USER_TOKEN,getToken())); 
    newHeaders = Collections.unmodifiableList(newHeaders); 

    Request r = new Request(
      request.getMethod(), 
      request.getUrl(), 
      newHeaders, 
      request.getBody() 
    ); 

    Timber.d("Request url: "+r.getUrl()); 
    Timber.d("Request new headers: "+r.getHeaders()); 

    return r; 
} 

/** 
* Do we have a token 
*/ 
private boolean hasToken(){ 
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); 
    return prefs.contains(Constants.TOKEN); 
} 

/** 
* Clear token 
*/ 
private void clearToken(){ 
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); 
    prefs.edit().remove(Constants.TOKEN).commit(); 
} 

/** 
* Saves token is prefs 
*/ 
private void saveToken(String token){ 
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); 
    prefs.edit().putString(Constants.TOKEN, token).commit(); 
    Timber.d("Saved new token: " + token); 
} 

/** 
* Gets token 
*/ 
private String getToken(){ 
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); 
    return prefs.getString(Constants.TOKEN,""); 
} 




/** 
* Refreshes the token by making login again, 
* //TODO implement refresh token endpoint, instead of making another login call 
*/ 
private synchronized void refreshToken(Request oldRequest) throws IOException{ 

    //We already have a token, it means a refresh call has already been made, get out 
    if(hasToken()) return; 

    Timber.d("We are going to refresh token"); 

    //Get credentials 
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); 
    String email = prefs.getString(Constants.EMAIL, ""); 
    String password = prefs.getString(Constants.PASSWORD, ""); 

    //Login again 
    com.app.bubbles.model.pojos.Response<Login> res = ((App) mContext).getApi().login(
      new com.app.bubbles.model.pojos.Request<>(credentials) 
    ); 

    //Save token in prefs 
    saveToken(res.data.getTokenContainer().getToken()); 

    Timber.d("Token refreshed"); 
} 


} 

我不知道改造/ OkHttpClient的體系結構深刻,但據我瞭解execute方法可以調用來自多個多次線程,OkClientCalls之間共享的只是淺拷貝完成。 我在refreshToken()方法中使用​​方法來避免多個線程進入refreshToken()並進行多次登錄調用,我需要一次刷新,只有一個線程應該使refreshCall和其他人將使用更新的令牌。

我還沒有認真測試過它,但是對於我能看到它工作正常。也許有人已經遇到過這個問題,可以分享他的解決方案,或者對於有相同/相似問題的人有幫助。

謝謝。

回答

7

對於任何人發現這一點,你應該OkHttp攔截去,或者使用驗證器API

這是改造的GitHub頁面

public void setup() { 
    OkHttpClient client = new OkHttpClient(); 
    client.interceptors().add(new TokenInterceptor(tokenManager)); 

    Retrofit retrofit = new Retrofit.Builder() 
      .addConverterFactory(GsonConverterFactory.create()) 
      .client(client) 
      .baseUrl("http://localhost") 
      .build(); 
} 

private static class TokenInterceptor implements Interceptor { 
    private final TokenManager mTokenManager; 

    private TokenInterceptor(TokenManager tokenManager) { 
     mTokenManager = tokenManager; 
    } 

    @Override 
    public Response intercept(Chain chain) throws IOException { 
     Request initialRequest = chain.request(); 
     Request modifiedRequest = request; 
     if (mTokenManager.hasToken()) { 
      modifiedRequest = request.newBuilder() 
        .addHeader("USER_TOKEN", mTokenManager.getToken()) 
        .build(); 
     } 

     Response response = chain.proceed(modifiedRequest); 
     boolean unauthorized = response.code() == 401; 
     if (unauthorized) { 
      mTokenManager.clearToken(); 
      String newToken = mTokenManager.refreshToken(); 
      modifiedRequest = request.newBuilder() 
        .addHeader("USER_TOKEN", mTokenManager.getToken()) 
        .build(); 
      return chain.proceed(modifiedRequest); 
     } 
     return response; 
    } 
} 

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

如果你想阻止的請求,直到身份驗證是一個樣本完成後,您可以使用與我的答案相同的同步機制,因爲攔截器可以在多個線程上同時運行

+1

並且如果要使用RX:http://stackoverflow.com/questions/25546934/retrofit-rxjava-and基於激情的服務 – Than

+0

@Sergio:謝謝你的精彩回答。不過,我的評論是針對您在問題中提供的代碼。只是好奇,如果通過使用Retrofit再次調用登錄來同步refreshToken,那麼它不會拋出NetworkOnMainThreadException,因爲Retrofit的同步調用位於主線程上,Android不允許網絡調用在主線上?提前致謝。 –

+0

@ShobhitPuri嗨,'refreshToken'方法在'execute'裏面調用,並且這個方法在後臺被Retrofit庫調用,現在不記得細節。但是做到這一點是安全的。 –

相關問題