OAuth2で保護されたサーバーと通信するために、AndroidアプリでRetrofitを使用しています。すべてがうまく機能します。RequestInterceptorを使用して、各呼び出しにアクセストークンを含めます。ただし、アクセストークンの有効期限が切れ、トークンを更新する必要がある場合があります。トークンの有効期限が切れると、次の呼び出しでUnauthorized HTTPコードが返されるため、簡単に監視できます。各レトロフィット呼び出しを次のように変更できます。失敗コールバックで、エラーコードがUnauthorizedに等しいかどうかを確認し、OAuthトークンを更新してから、レトロフィット呼び出しを繰り返します。ただし、このためには、すべての呼び出しを変更する必要がありますが、これは容易に保守できず、適切なソリューションではありません。すべてのレトロフィット呼び出しを変更せずにこれを行う方法はありますか?
認証の処理に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
の場合と同じ方法で、OkHttpClient
をInterceptors
に接続します。
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);
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 に言及しているように、これは危険な量の力です。
そうは言っても、これが今このようなことを処理する最良の方法だと私は確信しています。ご不明な点がございましたら、コメントでお気軽にお問い合わせください。
TokenAuthenticatorはサービスクラスに依存します。サービスクラスは、OkHttpClientインスタンスに依存します。 OkHttpClientを作成するには、TokenAuthenticatorが必要です。このサイクルを断ち切るにはどうすればよいですか? 2つの異なるOkHttpClient?それらは異なる接続プールを持つことになります。
たとえば、TokenService
の内部に必要なレトロフィットAuthenticator
がありますが、1つのOkHttpClient
のみをセットアップしたい場合は、TokenServiceHolder
をTokenAuthenticator
の依存関係として使用できます。アプリケーション(シングルトン)レベルで参照を維持する必要があります。 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または同様の依存性注入フレームワークを使用している場合、 この質問 への回答にいくつかの例があります
@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())
}
}
最後に、サービスを作成するときにInterceptor
とAuthenticator
をOKHttpClient
に追加しますPotoAuthApi
https://github.com/PhanVanLinh/AndroidMVPKotlin
getImage()
の例401エラーコードを返すauthenticate
内のTokenAuthenticator
メソッドはfiredになりますnoneAuthAPI.refreshToken(...)
と呼ばれる同期noneAuthAPI.refreshToken(...)
応答後->新しいトークンがヘッダーに追加されますgetImage()
will AUTO called新しいヘッダー付き(HttpLogging
WILL NOT log this call)(intercept
inside AuthInterceptor
WILL NOT CALLED)getImage()
がまだエラー401で失敗した場合、authenticate
内のTokenAuthenticator
メソッドはAGAINとAGAINを起動しますになり、呼び出しメソッドに関するエラーを何度もスローします(Java.net.ProtocolException: Too many follow-up requests
)。 count response で防ぐことができます。たとえば、return null
をauthenticate
に3回再試行した後、getImage()
はfinishおよびreturn response 401
になります
getImage()
応答成功=>の場合、結果は正常になります(エラーなしでgetImage()
を呼び出すように)
お役に立てば幸いです
私はこれが古いスレッドであることを知っていますが、万が一誰かがそれにつまずいた場合に備えて。
TokenAuthenticatorはサービスクラスに依存します。サービスクラスは、OkHttpClientインスタンスに依存します。 OkHttpClientを作成するには、TokenAuthenticatorが必要です。このサイクルを断ち切るにはどうすればよいですか? 2つの異なるOkHttpClient?それらは異なる接続プールを持つことになります。
私は同じ問題に直面していましたが、OkHttpClientを1つだけ作成したかったのは、TokenAuthenticator自体のために別のものが必要だとは思わないため、Dagger2を使用していたため、サービスクラスをTokenAuthenticatorの遅延インジェクション、ダガー2の遅延インジェクションの詳細を読むことができます こちら が、基本的に短剣NOTではなく、TokenAuthenticatorに必要なサービスをすぐに作成します。
サンプルコードについては、このSOスレッドを参照できます: まだDagger2を使用しながら循環依存関係を解決する方法は?
長い調査の後、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;
}
}
特定の例外をキャッチし、必要に応じて動作できるすべてのローダーの基本クラスを作成してみてください。動作を広めるために、すべての異なるローダーを基本クラスから拡張します。
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を返す最初の呼び出しを回避する方法が見つかりませんでした