2015-10-03 4 views
7

I Retrofit bin mit der Kommunikation mit dem Server-API, die API-Benutzer JSON Web-Token zur Authentifizierung zu behandeln. Die Token-Ablauf von Zeit zu Zeit, und ich bin auf der Suche nach dem besten Weg, einen Retrofit-Client zu implementieren, die das Token automatisch aktualisieren können, wenn es abläuft.Retrofit benutzerdefiniertes Client für WebTokens Authentifizierung

Dies ist die erste Implementierung kam ich mit,:

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


} 

Ich weiß nicht, die Architektur von Retrofit/OkHttpClient tief, aber so weit ich verstehe die Execute-Methode mehrere Male von mehreren aufgerufen werden kann Fäden, die OkClient das gleiche zwischen Calls nur eine flache Kopie geteilt wird getan. Ich synchronized in refreshToken() Verfahren unter Verwendung von mehreren Threads zu vermeiden, in refreshToken() und machen mehr Login Anrufe einzugeben, i ist ein Refresh nur ein Thread benötigt den refreshCall und die anderes machen soll, werden die erneuten Token verwenden.

ich habe es ernsthaft noch nicht getestet, aber für das, was ich es funktioniert gut sehen kann. Vielleicht hatte jemand dieses Problem bereits und kann seine Lösung teilen, oder es kann für jemanden mit dem gleichen/ähnlichen Problem hilfreich sein.

Danke.

Antwort

7

Für alle, die diese zu finden, sollten Sie mit OkHttp Interceptor gehen oder die Authenticator API

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

Wenn Sie Anfragen zu blockieren, bis die Authentifizierung

Dies ist ein Beispiel von Retrofit GitHub Seite verwenden fertig, können Sie den gleichen Synchronisationsmechanismus verwenden, den ich in meiner Antwort getan habe, da Interceptors gleichzeitig auf mehreren Threads ausgeführt werden können

+1

Und wenn Sie RX verwenden möchten: http://stackoverflow.com/questions/25546934/retrofit-rxjava-and -session-based-services – Than

+0

@Sergio: Danke für die wundervolle Antwort. Mein Kommentar bezieht sich jedoch auf den Code, den Sie in der Frage haben. wenn Sie nur neugierig, ein synchron zum 'refreshToken' durch Login Aufruf wieder mit' Retrofit', dauerte es nicht 'NetworkOnMainThreadException' werfen, da der synchrone Aufruf von Retrofit gemacht auf dem Hauptthread und Android erlaubt keine Netzanrufe auf Hauptgewinde? Danke im Voraus. –

+0

@ShobhitPuri Hallo, die 'refreshToken' Methode wird innerhalb' execute' aufgerufen und diese Methode wird im Hintergrund von Retrofit library aufgerufen, erinnere mich nicht an die Details jetzt. Aber es ist sicher, es zu tun. –

Verwandte Themen