前提
Retrofitを使ったAPI通信時に、OAuthトークンの取得やリフレッシュをどこでやるのが適切か検討した。
結果、2つのアプローチを考えついた。
- RxJavaのonErrorResumeNextを利用したアプローチ
- 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
以上。