Help us understand the problem. What is going on with this article?

Retrofitを使ったOAuth再認証アプローチ

More than 1 year has passed since last update.

前提

Retrofitを使ったAPI通信時に、OAuthトークンの取得やリフレッシュをどこでやるのが適切か検討した。
結果、2つのアプローチを考えついた。

  1. RxJavaのonErrorResumeNextを利用したアプローチ
  2. OkHttpのAuthenticatorを使ったアプローチ

結論としては自分は後者を採用したが、いずれも有効な手法だと考えたためメモとして残す。

なお、本記事は RxJava+RetrofitでAPI通信周りを実装するうえで最低限の知識を30分で詰め込む の発展系という位置づけだが、独立した記事として読める。

準備

いずれのアプローチも、HTTPリクエスト時にOAuthヘッダを自動で付加したいため、次のような okhttp3.Interceptor を用意する。
なおここで tokenRepository.getToken() はローカルに保存されたトークンを読み出すものとする。

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

    // TODO error handling for unrecoverable situation
    Token token = tokenRepository
            .getToken()
            .blockingGet();

    request = request.newBuilder()
            .header("Authorization", "Bearer " + token.getAccessToken())
            .build();

    return chain.proceed(request);
  }
}

見ての通り、リクエストをinterceptしてAuthorizationヘッダを付加している。
あとは言うまでもないが、

OkHttpClient client = new OkHttpClient().newBuilder()
    .addInterceptor(new OAuthHeaderInterceptor())
    .build();

return new Retrofit.Builder()
    .client(client)
    .baseUrl("https://www.examples.com/api/")
    .build();

のようにしてRetrofitインスタンスを得ればよい。

RxJavaのonErrorResumeNextを利用したアプローチ

onErrorResumeNext

onExceptionResumeNextは、上流の Observable<T> をミラーするが、エラー時のみ任意のハンドリングをしたのちに Observable<T> を作って下流に流すためのオペレータである。

エラー時のThrowableも渡されるので、次のようにHTTP 401時に再認証を掛けたのち、再度上流のObservableをリトライすれば目的を達成できそうだ。

public <T> Function<Throwable, SingleSource<? extends T>> refreshTokenAndRetry(final Single<T> toBeResumed) {
  return throwable -> {
    if (throwable instanceof HttpException) {
      HttpException exception = (HttpException) throwable;
      if (exception.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
        Single<Token> tokenPromise = tokenRepository
              .getFromRemote()
              .doOnSuccess(tokenRepository::saveToLocal);
        return tokenPromise.flatMap(token -> toBeResumed);
      }
    }
    return Single.error(throwable);
  };
}

tokenRepository.getFromRemote().doOnSuccess(tokenRepository::saveToLocal) でローカルストレージに保存したのち、flatMapでつないで上流のObservableをそのまま返している。
(ヘッダは前出のOAuthHeaderInterceptorが透過的にセットしなおす)

これで次のようにsubscribeすればトークンを更新した上で再度API呼び出しをしてくれる。

service.getFoo()
    .subscribeOn(Schedulers.io())
    .onErrorResumeNext(refreshTokenAndRetry(service.getFoo()))
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(this::processResult);

これは意図通り動いてくれる。

SingleTransformer

SingleTransformerを活用するともう少しシンプルにすることができる。
先程の refreshTokenAndRetry() 関数を次のように移植してみる。

public class TokenRefreshTransformer<T> implements SingleTransformer<T, T> {
  @Override
  public SingleSource<T> apply(@NonNull Single<T> upstream) {
    return upstream.onErrorResumeNext(refreshTokenAndRetry(upstream));
  }

  private Function<Throwable, SingleSource<? extends T>> refreshTokenAndRetry(final Single<T> toBeResumed) {
    return throwable -> {
      if (throwable instanceof HttpException) {
        HttpException exception = (HttpException) throwable;
        if (exception.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
          Single<Token> tokenPromise = tokenRepository
                .getFromRemote()
                .doOnSuccess(tokenRepository::saveToLocal);
          return tokenPromise.flatMap(token -> toBeResumed);
        }
      }
      return Single.error(throwable);
    };
  }
}

ObservableTransformerは上流のObservableを任意に加工し下流のObservableに流すための仕組みである(今回はSingleを使ったのでSingleTransformer)。
見ての通り、これまでの処理をそのまま閉じ込めただけにすぎない。

service.getFoo()
    .subscribeOn(Schedulers.io())
    .compose(new TokenRefreshTransformer<>())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(this::processResult);

呼び出す側は compose() メソッドでつなぐだけだ。
中身の型 T は推論が効くし、これでさらにシンプルになった。悪くなさそうだ。

OkHttpのAuthenticatorを使ったアプローチ

次にOkHttpのレイヤで再認証するアプローチについて考察する。
OkHttpはInterceptorという素晴らしい仕組みでHTTPリクエスト/レスポンスをinterceptして処理をはさむことができる。

Interceptorは冒頭の「準備」でOAuthヘッダの差し込みにすでに利用した。
この仕組みを使ってRxJavaより下のレイヤで再認証をすることも可能だ。たとえばこのgistが参考になる。

ただ、OkHttpはAuthenticatorというそれ専用のフックポイントを用意している。
端的に言うと、HTTP 401 Not Authorized, HTTP 407 Proxy Authentication Required のように認証が必要なレスポンスが返ってきたときに処理を差し込むことができる。

public class TokenRefreshAuthenticator implements Authenticator {
  @Override
  public Request authenticate(Route route, Response response) throws IOException {
    // TODO error handling for unrecoverable situation
    Token token = tokenRepository
            .getToken()
            .blockingGet();
    return response.request().newBuilder()
            .header("Authorization", "Bearer " + token.getAccessToken())
            .build();
  }
}

ここで返却したRequestでリトライしてくれるので非常に便利だ。
なお、ここでは再認証を諦めるロジックについては割愛しているので、トークンリフレッシュ処理で適切にエラーハンドリングするなり適宜リトライ回数を制限するなりする必要がある。

あとはこのAuthenticatorをOkHttpに登録するだけだ。

OkHttpClient client = new OkHttpClient().newBuilder()
    .addInterceptor(new OAuthHeaderInterceptor())
    .authenticator(new TokenRefreshAuthenticator()) // HERE!
    .build();

return new Retrofit.Builder()
    .client(client)
    .baseUrl("https://www.examples.com/api/")
    .build();

この方法はSingleTransformerを使うまでもなく透過的に機能してくれるので非常に便利に感じている。
詳しいレシピはOkHttpのサイトの Handling authentication に解説されているので適宜参照して欲しい。

余談

好みによって何を使うかは分かれそうだが他にも良い方法があったら是非コメントしていただきたい。

なお、RxJavaを使うアプローチで、 retryWhen() オペレータを使いかつKotlinの拡張関数でかなり読みやすく再認証をする方法を紹介している面白いエントリがあった。参考までに紹介する。

参考) retrofit with rxjava handling network exceptions globally

以上。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away