2015-10-03 14 views
7

Używam funkcji Retrofit do obsługi komunikacji z interfejsem API serwera, JSON Web Token użytkownika interfejsu API do uwierzytelniania. Token wygasa od czasu do czasu i szukam najlepszego sposobu wdrożenia klienta aktualizacji, który może odświeżać token automatycznie po jego wygaśnięciu.Retrofit klienta niestandardowego do uwierzytelniania WebTokens

Jest to wstępna implementacja wymyśliłem,:

/** 
* 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"); 
} 


} 

nie wiem architekturę modernizacyjny/OkHttpClient głęboko, ale o ile mogę zrozumieć sposób wykonać można nazwać wiele razy z wielu wątków,jest taki sam udostępniony między Calls tylko płytka kopia jest wykonywana. Używam metody synchronized w refreshToken(), aby uniknąć wielu wątków, aby wprowadzić w refreshToken() i wykonać wiele połączeń logowania, i odświeżenie jest potrzebne tylko jeden wątek powinien uczynić refreshCall, a inni będą używać odnowionego tokena.

Nie testowałem tego jeszcze poważnie, ale to, co widzę, działa dobrze. Może ktoś już miał ten problem i może dzielić się swoim rozwiązaniem lub może być pomocny dla kogoś z tym samym/podobnym problemem.

Dzięki.

Odpowiedz

7

Dla każdego, że wybrać ten, należy udać się z OkHttp przechwytujących lub użyj Authenticator API

Jest to próbka z Doposażenie GitHub stronie

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

Jeśli chcesz blokować żądania aż uwierzytelniania jest zrobione, możesz użyć tego samego mechanizmu synchronizacji, co zrobiłem w mojej odpowiedzi, ponieważ przechwytywacze mogą działać jednocześnie na wielu wątkach

+1

A jeśli chcesz używać RX: http://stackoverflow.com/questions/25546934/retrofit-rxjava-and -sesja-usługi-usługi – Than

+0

@Sergio: Dzięki za cudowną odpowiedź. Jednak mój komentarz odnosi się do kodu, który masz w pytaniu. Po prostu ciekawy, czy dokonałeś synchronizacji z 'refreshToken' przez ponowne wywołanie loginu za pomocą' Retrofit', czy nie wyrzucił 'NetworkOnMainThreadException', ponieważ wywołanie synchroniczne wykonane przez Retrofit znajduje się w głównym wątku, a Android nie zezwala na połączenia sieciowe w głównym wątku? Z góry dziękuję. –

+0

@ShobhitPuri Cześć, metoda 'refreshToken' jest wywoływana wewnątrz' execute' i ta metoda jest wywoływana w tle przez bibliotekę Retrofit, teraz nie pamiętam szczegółów. Ale można to bezpiecznie zrobić. –