web-dev-qa-db-ja.com

すべての呼び出しを変更せずにRetrofitを使用してOAuthトークンを更新する

OAuth2で保護されたサーバーと通信するために、AndroidアプリでRetrofitを使用しています。すべてがうまく機能します。RequestInterceptorを使用して、各呼び出しにアクセストークンを含めます。ただし、アクセストークンの有効期限が切れ、トークンを更新する必要がある場合があります。トークンの有効期限が切れると、次の呼び出しでUnauthorized HTTPコードが返されるため、簡単に監視できます。各レトロフィット呼び出しを次のように変更できます。失敗コールバックで、エラーコードがUnauthorizedに等しいかどうかを確認し、OAuthトークンを更新してから、レトロフィット呼び出しを繰り返します。ただし、このためには、すべての呼び出しを変更する必要がありますが、これは容易に保守できず、適切なソリューションではありません。すべてのレトロフィット呼び出しを変更せずにこれを行う方法はありますか?

138
Daniel Zolnai

認証の処理にInterceptorsを使用しないでください。

現在、認証を処理する最良の方法は、 この目的 専用に設計された新しい Authenticator APIを使用することです。

OkHttpは、応答が401 Not Authorisedである場合、資格情報のAuthenticator自動的に要求する最後に失敗した要求を再試行するを使用します。

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

Authenticatorの場合と同じ方法で、OkHttpClientInterceptorsに接続します。

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

Retrofitを作成するときにこのクライアントを使用しますRestAdapter

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

Retrofit > = 1.9.0を使用している場合、 OkHttp's new Interceptor を使用できます。これはOkHttp 2.2.0で導入されました。 Application Interceptor を使用すると、retry and make multiple callsが許可されます。

インターセプターは、次の擬似コードのようになります。

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

}

Interceptorを定義したら、OkHttpClientを作成し、インターセプターを Application Interceptor として追加します。

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

そして最後に、OkHttpClientを作成するときにこのRestAdapterを使用します。

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

警告:Jesse Wilson(Squareから)が here に言及しているように、これは危険な量の力です。

そうは言っても、これが今このようなことを処理する最良の方法だと私は確信しています。ご不明な点がございましたら、コメントでお気軽にお問い合わせください。

62
theblang

TokenAuthenticatorはサービスクラスに依存します。サービスクラスは、OkHttpClientインスタンスに依存します。 OkHttpClientを作成するには、TokenAuthenticatorが必要です。このサイクルを断ち切るにはどうすればよいですか? 2つの異なるOkHttpClient?それらは異なる接続プールを持つことになります。

たとえば、TokenServiceの内部に必要なレトロフィットAuthenticatorがありますが、1つのOkHttpClientのみをセットアップしたい場合は、TokenServiceHolderTokenAuthenticatorの依存関係として使用できます。アプリケーション(シングルトン)レベルで参照を維持する必要があります。 Dagger 2を使用している場合、これは簡単です。それ以外の場合は、アプリケーション内にクラスフィールドを作成します。

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

TokenServiceHolder.Java ::

public class TokenServiceHolder {

    TokenService tokenService = null;

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

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

クライアントのセットアップ:

//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);

Dagger 2または同様の依存性注入フレームワークを使用している場合、 この質問 への回答にいくつかの例があります

21
David Rawson

@theblang answerのようなTokenAuthenticatorを使用することは、refresh_tokenを処理するための正しい方法です。

ここに私の道具があります(私はKotlin、Dagger、RXを使用していますが、このアイデアをあなたのケースに実装するために使用できます)
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()
    }
}

依存サイクル @Brais Gabinコメントのように防ぐため、2のようなインターフェイスを作成します

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>
}

そして

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

AccessTokenWrapperクラス

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クラス

data class AccessToken(
        @Expose
        var token: String,

        @Expose
        var refreshToken: String)

マイインターセプター

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

最後に、サービスを作成するときにInterceptorAuthenticatorOKHttpClientに追加しますPotoAuthApi

デモ

https://github.com/PhanVanLinh/AndroidMVPKotlin

注意

  • API getImage()の例401エラーコードを返す
  • authenticate内のTokenAuthenticatorメソッドはfiredになります
  • noneAuthAPI.refreshToken(...)と呼ばれる同期
  • noneAuthAPI.refreshToken(...)応答後->新しいトークンがヘッダーに追加されます
  • getImage() will AUTO called新しいヘッダー付き(HttpLoggingWILL NOT log this call)(intercept inside AuthInterceptorWILL NOT CALLED
  • getImage()がまだエラー401で失敗した場合、authenticate内のTokenAuthenticatorメソッドはAGAINとAGAINを起動しますになり、呼び出しメソッドに関するエラーを何度もスローします(Java.net.ProtocolException: Too many follow-up requests)。 count response で防ぐことができます。たとえば、return nullauthenticateに3回再試行した後、getImage()finishおよびreturn response 401になります

  • getImage()応答成功=>の場合、結果は正常になります(エラーなしでgetImage()を呼び出すように)

お役に立てば幸いです

2
Phan Van Linh

私はこれが古いスレッドであることを知っていますが、万が一誰かがそれにつまずいた場合に備えて。

TokenAuthenticatorはサービスクラスに依存します。サービスクラスは、OkHttpClientインスタンスに依存します。 OkHttpClientを作成するには、TokenAuthenticatorが必要です。このサイクルを断ち切るにはどうすればよいですか? 2つの異なるOkHttpClient?それらは異なる接続プールを持つことになります。

私は同じ問題に直面していましたが、OkHttpClientを1つだけ作成したかったのは、TokenAuthenticator自体のために別のものが必要だとは思わないため、Dagger2を使用していたため、サービスクラスをTokenAuthenticatorの遅延インジェクション、ダガー2の遅延インジェクションの詳細を読むことができます こちら が、基本的に短剣NOTではなく、TokenAuthenticatorに必要なサービスをすぐに作成します。

サンプルコードについては、このSOスレッドを参照できます: まだDagger2を使用しながら循環依存関係を解決する方法は?

1
Boda

長い調査の後、Apacheクライアントをカスタマイズして、アクセストークンをパラメーターとして送信する、RetrofitのRefreshing AccessTokenを処理しました。

Cookie Persistent Clientを使用してアダプターを開始する

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

Cookie永続クライアントは、すべてのリクエストのCookieを維持し、不正なアクセスERROR_CODE = 401の場合、リクエストレスポンスごとにチェックし、アクセストークンを更新してリクエストをリコールします。

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

特定の例外をキャッチし、必要に応じて動作できるすべてのローダーの基本クラスを作成してみてください。動作を広めるために、すべての異なるローダーを基本クラスから拡張します。

0
k3v1n4ud3

1つのインターセプター(トークンを挿入)と1つのオーセンティケーター(更新操作)を使用してジョブを実行しますが、

私も二重呼び出しの問題がありました:最初の呼び出しは常に401を返しました:最初の呼び出し(インターセプター)でトークンが挿入されず、認証子が呼び出されました:2つの要求が行われました。

修正は、Interceptorのビルドへのリクエストに再度影響を与えることでした。

前:

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

後:

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

1つのブロックで:

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

それが役に立てば幸い。

編集:認証子のみを使用してインターセプターを使用せずに常に401を返す最初の呼び出しを回避する方法が見つかりませんでした

0
Sigrun