wake-up-neo.com

Aktualisieren OAuth Token mit Retrofit, ohne alle Anrufe zu ändern

Wir verwenden Retrofit in unserer App Android=), um mit einem OAuth2-gesicherten Server zu kommunizieren. Alles funktioniert einwandfrei. Wir verwenden den RequestInterceptor, um den Zugriffstoken bei jedem Aufruf einzuschließen. Es wird jedoch Zeiten geben, in denen Das Zugriffstoken läuft ab und das Token muss aktualisiert werden. Wenn das Token abläuft, wird der nächste Aufruf mit einem nicht autorisierten HTTP-Code zurückgegeben, sodass dies einfach zu überwachen ist. Wir könnten jeden Retrofit-Aufruf folgendermaßen modifizieren: Im Fehlerfall Rückruf Überprüfen Sie, ob der Fehlercode gleich Unauthorized ist, aktualisieren Sie das OAuth= Token und wiederholen Sie dann den Retrofit-Aufruf. Hierzu sollten jedoch alle Aufrufe geändert werden, was nicht einfach zu warten ist. und eine gute Lösung. Gibt es eine Möglichkeit, dies zu tun, ohne alle Retrofit-Aufrufe zu ändern?

138
Daniel Zolnai

Bitte benutzen Sie Interceptors nicht, um sich mit der Authentifizierung zu befassen.

Derzeit ist die Verwendung der neuen Authenticator -API, die speziell für diesen Zweck entwickelt wurde, die beste Methode für die Authentifizierung.

OkHttp fragt automatisch den Authenticator nach Anmeldeinformationen, wenn eine Antwort 401 Not Authorised Wiederholung der letzten fehlgeschlagenen Anfrage mit ihnen.

public class TokenAuthenticator implements Authenticator {
    @Override
    public Request authenticate(Proxy proxy, Response response) throws IOException {
        // Refresh your access_token using a synchronous api request
        newAccessToken = service.refreshToken();

        // Add new header to rejected request and retry it
        return response.request().newBuilder()
                .header(AUTHORIZATION, newAccessToken)
                .build();
    }

    @Override
    public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
        // Null indicates no attempt to authenticate.
        return null;
    }

Hängen Sie ein Authenticator an ein OkHttpClient wie bei Interceptors

OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setAuthenticator(authAuthenticator);

Verwenden Sie diesen Client beim Erstellen Ihres RetrofitRestAdapter

RestAdapter restAdapter = new RestAdapter.Builder()
                .setEndpoint(ENDPOINT)
                .setClient(new OkClient(okHttpClient))
                .build();
return restAdapter.create(API.class);
190
lgvalle

Wenn Sie Retrofit > = 1.9.0 verwenden, können Sie OkHttp's new Interceptor verwenden, das in OkHttp 2.2.0 eingeführt wurde. Sie möchten einen Application Interceptor verwenden, mit dem Sie retry and make multiple calls eingeben können.

Dein Interceptor könnte ungefähr so ​​aussehen:

public class CustomInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        // try the request
        Response response = chain.proceed(request);

        if (response shows expired token) {

            // get a new token (I use a synchronous Retrofit call)

            // create a new request and modify it accordingly using the new token
            Request newRequest = request.newBuilder()...build();

            // retry the request
            return chain.proceed(newRequest);
        }

        // otherwise just pass the original response on
        return response;
    }

}

Nachdem Sie Ihr Interceptor definiert haben, erstellen Sie ein OkHttpClient und fügen Sie den Interceptor als Application Interceptor hinzu.

    OkHttpClient okHttpClient = new OkHttpClient();
    okHttpClient.interceptors().add(new CustomInterceptor());

Und schließlich verwenden Sie dieses OkHttpClient, wenn Sie Ihr RestAdapter erstellen.

    RestService restService = new RestAdapter().Builder
            ...
            .setClient(new OkClient(okHttpClient))
            .create(RestService.class);

Warnung: Wie Jesse Wilson (von Square) hier erwähnt, ist dies eine gefährliche Menge an Kraft.

Nach alledem denke ich definitiv, dass dies der beste Weg ist, jetzt mit so etwas umzugehen. Wenn Sie Fragen haben, zögern Sie bitte nicht, einen Kommentar abzugeben.

62
theblang

TokenAuthenticator hängt von einer Serviceklasse ab. Die Serviceklasse hängt von einer OkHttpClient-Instanz ab. Um einen OkHttpClient zu erstellen, benötige ich den TokenAuthenticator. Wie kann ich diesen Zyklus unterbrechen? Zwei verschiedene OkHttpClients? Sie werden verschiedene Verbindungspools haben.

Wenn Sie beispielsweise eine Nachrüstung TokenService haben, die Sie in Ihrer Authenticator benötigen, aber nur eine OkHttpClient einrichten möchten, können Sie eine TokenServiceHolder verwenden. als Abhängigkeit für TokenAuthenticator. Sie müssten auf Anwendungsebene (Singleton) einen Verweis darauf pflegen. Dies ist einfach, wenn Sie Dolch 2 verwenden, andernfalls erstellen Sie einfach ein Klassenfeld in Ihrer Anwendung.

Im TokenAuthenticator.Java

public class TokenAuthenticator implements Authenticator {

    private final TokenServiceHolder tokenServiceHolder;

    public TokenAuthenticator(TokenServiceHolder tokenServiceHolder) {
        this.tokenServiceHolder = tokenServiceHolder;
    }

    @Override
    public Request authenticate(Proxy proxy, Response response) throws IOException {

        //is there a TokenService?
        TokenService service = tokenServiceHolder.get();
        if (service == null) {
            //there is no way to answer the challenge
            //so return null according to Retrofit's convention
            return null;
        }

        // Refresh your access_token using a synchronous api request
        newAccessToken = service.refreshToken().execute();

        // Add new header to rejected request and retry it
        return response.request().newBuilder()
                .header(AUTHORIZATION, newAccessToken)
                .build();
    }

    @Override
    public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
        // Null indicates no attempt to authenticate.
        return null;
    }

Im TokenServiceHolder.Java:

public class TokenServiceHolder {

    TokenService tokenService = null;

    @Nullable
    public TokenService get() {
        return tokenService;
    }

    public void set(TokenService tokenService) {
        this.tokenService = tokenService;
    }
}

Client-Setup:

//obtain instance of TokenServiceHolder from application or singleton-scoped component, then
TokenAuthenticator authenticator = new TokenAuthenticator(tokenServiceHolder);
OkHttpClient okHttpClient = new OkHttpClient();    
okHttpClient.setAuthenticator(tokenAuthenticator);

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .client(okHttpClient)
    .build();

TokenService tokenService = retrofit.create(TokenService.class);
tokenServiceHolder.set(tokenService);

Wenn Sie Dagger 2 oder ein ähnliches Framework für die Abhängigkeitsinjektion verwenden, finden Sie in den Antworten auf diese Frage einige Beispiele

21
David Rawson

Die Verwendung von TokenAuthenticator wie @theblang answer ist ein korrekter Weg, um mit refresh_token Umzugehen.

Hier ist mein Gerät (ich verwende Kotlin, Dagger, RX, aber Sie können diese Idee für das Gerät in Ihrem Fall verwenden)
TokenAuthenticator

class TokenAuthenticator @Inject constructor(private val noneAuthAPI: PotoNoneAuthApi, private val accessTokenWrapper: AccessTokenWrapper) : Authenticator {

    override fun authenticate(route: Route, response: Response): Request? {
        val newAccessToken = noneAuthAPI.refreshToken(accessTokenWrapper.getAccessToken()!!.refreshToken).blockingGet()
        accessTokenWrapper.saveAccessToken(newAccessToken) // save new access_token for next called
        return response.request().newBuilder()
                .header("Authorization", newAccessToken.token) // just only need to override "Authorization" header, don't need to override all header since this new request is create base on old request
                .build()
    }
}

Um einen Abhängigkeitszyklus wie den @Brais Gabin-Kommentar zu verhindern , erstelle ich 2 Schnittstellen mögen

interface PotoNoneAuthApi { // NONE authentication API
    @POST("/login")
    fun login(@Body request: LoginRequest): Single<AccessToken>

    @POST("refresh_token")
    @FormUrlEncoded
    fun refreshToken(@Field("refresh_token") refreshToken: String): Single<AccessToken>
}

und

interface PotoAuthApi { // Authentication API
    @GET("api/images")
    fun getImage(): Single<GetImageResponse>
}

AccessTokenWrapper Klasse

class AccessTokenWrapper constructor(private val sharedPrefApi: SharedPrefApi) {
    private var accessToken: AccessToken? = null

    // get accessToken from cache or from SharePreference
    fun getAccessToken(): AccessToken? {
        if (accessToken == null) {
            accessToken = sharedPrefApi.getObject(SharedPrefApi.ACCESS_TOKEN, AccessToken::class.Java)
        }
        return accessToken
    }

    // save accessToken to SharePreference
    fun saveAccessToken(accessToken: AccessToken) {
        this.accessToken = accessToken
        sharedPrefApi.putObject(SharedPrefApi.ACCESS_TOKEN, accessToken)
    }
}

AccessToken Klasse

data class AccessToken(
        @Expose
        var token: String,

        @Expose
        var refreshToken: String)

Mein Abfangjäger

class AuthInterceptor @Inject constructor(private val accessTokenWrapper: AccessTokenWrapper): Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val authorisedRequestBuilder = originalRequest.newBuilder()
                .addHeader("Authorization", accessTokenWrapper.getAccessToken()!!.token)
                .header("Accept", "application/json")
        return chain.proceed(authorisedRequestBuilder.build())
    }
}

Fügen Sie abschließend Interceptor und Authenticator zu Ihrem OKHttpClient hinzu, wenn Sie einen Service erstellen PotoAuthApi

Demo

https://github.com/PhanVanLinh/AndroidMVPKotlin

Hinweis

  • Beispiel-API getImage() gibt den Fehlercode 401 zurück
  • authenticate Methode in TokenAuthenticator wird ausgelöst
  • Synchronize noneAuthAPI.refreshToken(...) wird aufgerufen
  • Nach noneAuthAPI.refreshToken(...) response -> wird ein neues Token zum Header hinzugefügt
  • getImage() wird AUTO aufgerufen mit neuem Header (HttpLogging WIRD NICHT protokolliert dieser Aufruf) (intercept innerhalb von AuthInterceptor wird nicht aufgerufen )
  • Wenn getImage() immer noch mit Fehler 401 fehlgeschlagen ist, wird die authenticate -Methode in TokenAuthenticator erneut und erneut ausgelöst. ) dann wird es viele Male einen Fehler über die Aufrufmethode auslösen (Java.net.ProtocolException: Too many follow-up requests). Sie können dies verhindern, indem Sie Antwort zählen . Beispiel: Wenn Sie return null In authenticate nach dreimaligem erneutem Versuch ausführen, wird getImage() beendet und return response 401

  • Wenn getImage() response success => wird das Ergebnis normal ausgegeben (wie Sie getImage() ohne Fehler aufrufen)

Hoffe es hilft

2
Phan Van Linh

Ich weiß, dass dies ein alter Thread ist, aber nur für den Fall, dass jemand darüber gestolpert ist.

TokenAuthenticator hängt von einer Serviceklasse ab. Die Serviceklasse hängt von einer OkHttpClient-Instanz ab. Um einen OkHttpClient zu erstellen, benötige ich den TokenAuthenticator. Wie kann ich diesen Zyklus unterbrechen? Zwei verschiedene OkHttpClients? Sie werden verschiedene Verbindungspools haben.

Ich hatte das gleiche Problem, aber ich wollte nur einen OkHttpClient erstellen, da ich glaube nicht, dass ich nur für den TokenAuthenticator selbst einen anderen brauche. Ich habe Dagger2 verwendet, also habe ich die Serviceklasse als Lazy injected Im TokenAuthenticator können Sie mehr über Lazy injecting in Dolch 2 lesen hier , aber es ist so, als würde man Dagger sagen, [~ # ~] nicht [~ # ~] Erstellen Sie sofort den Service, den der TokenAuthenticator benötigt.

Sie können auf diesen Thread SO für Beispielcode verweisen: So lösen Sie eine zirkuläre Abhängigkeit auf, während Sie noch Dagger2 verwenden?

1
Boda

Nach langer Recherche habe ich den Apache-Client so angepasst, dass er Refreshing AccessToken For Retrofit handhabt, in dem Sie das Zugriffstoken als Parameter senden.

Initiieren Sie Ihren Adapter mit dem Cookie Persistent Client

restAdapter = new RestAdapter.Builder()
                .setEndpoint(SERVER_END_POINT)
                .setClient(new CookiePersistingClient())
                .setLogLevel(RestAdapter.LogLevel.FULL).build();

Cookie Persistenter Client, der Cookies für alle Anforderungen verwaltet und bei jeder Antwort auf eine Anforderung überprüft, wenn es sich um einen nicht autorisierten Zugriff ERROR_CODE = 401 handelt, das Zugriffstoken aktualisiert und die Anforderung zurückruft, andernfalls nur die Anforderung verarbeitet.

private static class CookiePersistingClient extends ApacheClient {

    private static final int HTTPS_PORT = 443;
    private static final int SOCKET_TIMEOUT = 300000;
    private static final int CONNECTION_TIMEOUT = 300000;

    public CookiePersistingClient() {
        super(createDefaultClient());
    }

    private static HttpClient createDefaultClient() {
        // Registering https clients.
        SSLSocketFactory sf = null;
        try {
            KeyStore trustStore = KeyStore.getInstance(KeyStore
                    .getDefaultType());
            trustStore.load(null, null);

            sf = new MySSLSocketFactory(trustStore);
            sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (UnrecoverableKeyException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        HttpParams params = new BasicHttpParams();
        HttpConnectionParams.setConnectionTimeout(params,
                CONNECTION_TIMEOUT);
        HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT);
        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("https", sf, HTTPS_PORT));
        // More customization (https / timeouts etc) can go here...

        ClientConnectionManager cm = new ThreadSafeClientConnManager(
                params, registry);
        DefaultHttpClient client = new DefaultHttpClient(cm, params);

        // Set the default cookie store
        client.setCookieStore(COOKIE_STORE);

        return client;
    }

    @Override
    protected HttpResponse execute(final HttpClient client,
            final HttpUriRequest request) throws IOException {
        // Set the http context's cookie storage
        BasicHttpContext mHttpContext = new BasicHttpContext();
        mHttpContext.setAttribute(ClientContext.COOKIE_STORE, COOKIE_STORE);
        return client.execute(request, mHttpContext);
    }

    @Override
    public Response execute(final Request request) throws IOException {
        Response response = super.execute(request);
        if (response.getStatus() == 401) {

            // Retrofit Callback to handle AccessToken
            Callback<AccessTockenResponse> accessTokenCallback = new Callback<AccessTockenResponse>() {

                @SuppressWarnings("deprecation")
                @Override
                public void success(
                        AccessTockenResponse loginEntityResponse,
                        Response response) {
                    try {
                        String accessToken =  loginEntityResponse
                                .getAccessToken();
                        TypedOutput body = request.getBody();
                        ByteArrayOutputStream byte1 = new ByteArrayOutputStream();
                        body.writeTo(byte1);
                        String s = byte1.toString();
                        FormUrlEncodedTypedOutput output = new FormUrlEncodedTypedOutput();
                        String[] pairs = s.split("&");
                        for (String pair : pairs) {
                            int idx = pair.indexOf("=");
                            if (URLDecoder.decode(pair.substring(0, idx))
                                    .equals("access_token")) {
                                output.addField("access_token",
                                        accessToken);
                            } else {
                                output.addField(URLDecoder.decode(
                                        pair.substring(0, idx), "UTF-8"),
                                        URLDecoder.decode(
                                                pair.substring(idx + 1),
                                                "UTF-8"));
                            }
                        }
                        execute(new Request(request.getMethod(),
                                request.getUrl(), request.getHeaders(),
                                output));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }

                @Override
                public void failure(RetrofitError error) {
                    // Handle Error while refreshing access_token
                }
            };
            // Call Your retrofit method to refresh ACCESS_TOKEN
            refreshAccessToken(GRANT_REFRESH,CLIENT_ID, CLIENT_SECRET_KEY,accessToken, accessTokenCallback);
        }

        return response;
    }
}
0
Suneel Prakash

Sie können versuchen, eine Basisklasse für alle Ihre Loader zu erstellen, in der Sie eine bestimmte Ausnahme abfangen und dann nach Bedarf handeln können. Stellen Sie sicher, dass alle Ihre verschiedenen Lader von der Basisklasse ausgehen, um das Verhalten zu verbreiten.

0
k3v1n4ud3

Verwenden Sie einen Interceptor (Injizieren des Tokens) und einen Authenticator (Aktualisierungsvorgänge), um folgende Aufgaben auszuführen:

Ich hatte auch ein Problem mit zwei Anrufen: der erste Anruf ergab immer einen 401: Das Token wurde beim ersten Aufruf (Interceptor) nicht injiziert, und der Authentifikator wurde aufgerufen: Es wurden zwei Anforderungen gestellt.

Der Fix bestand nur darin, die Anfrage an den Build im Interceptor erneut zu bearbeiten:

VOR:

private Interceptor getInterceptor() {
    return (chain) -> {
        Request request = chain.request();
        //...
        request.newBuilder()
                .header(AUTHORIZATION, token))
                .build();
        return chain.proceed(request);
    };
}

NACH:

private Interceptor getInterceptor() {
    return (chain) -> {
        Request request = chain.request();
        //...
        request = request.newBuilder()
                .header(AUTHORIZATION, token))
                .build();
        return chain.proceed(request);
    };
}

IN EINEM BLOCK:

private Interceptor getInterceptor() {
    return (chain) -> {
        Request request = chain.request().newBuilder()
                .header(AUTHORIZATION, token))
                .build();
        return chain.proceed(request);
    };
}

Ich hoffe es hilft.

Edit: Ich habe keine Möglichkeit gefunden, dem ersten Aufruf zu entgehen, immer nur den Authentifikator und keinen Interceptor zu verwenden, um 401 zurückzugeben

0
Sigrun