ViewPager
があり、ViewPager
が同時にロードされると3つのWebサービス呼び出しが行われます。
最初のリクエストが401を返すと、Authenticator
が呼び出され、Authenticator
内のトークンを更新しますが、残りの2つのリクエストは古いリフレッシュトークンで既にサーバーに送信され、Interceptorでキャプチャされた498で失敗しますアプリがログアウトされます。
これは私が期待する理想的な動作ではありません。 2番目と3番目の要求をキューに保持し、トークンが更新されたら、キューに入れられた要求を再試行します。
現在、Authenticator
でトークンの更新が進行中かどうかを示す変数があります。その場合、Interceptor
の後続のすべてのリクエストをキャンセルし、ユーザーはページを手動で更新するか、ログアウトできますユーザーとユーザーのログインを強制します。
Androidのokhttp 3.xを使用して上記の問題を解決するのに適したソリューションまたはアーキテクチャは何ですか?
編集:私が解決したい問題は一般的であり、私は私の呼び出しをシーケンスしたくない。つまり、1つの呼び出しが終了してトークンを更新するのを待ってから、アクティビティレベルとフラグメントレベルで残りのリクエストのみを送信します。
コードが要求されました。これはAuthenticator
の標準コードです:
public class CustomAuthenticator implements Authenticator {
@Inject AccountManager accountManager;
@Inject @AccountType String accountType;
@Inject @AuthTokenType String authTokenType;
@Inject
public ApiAuthenticator(@ForApplication Context context) {
}
@Override
public Request authenticate(Route route, Response response) throws IOException {
// Invaidate authToken
String accessToken = accountManager.peekAuthToken(account, authTokenType);
if (accessToken != null) {
accountManager.invalidateAuthToken(accountType, accessToken);
}
try {
// Get new refresh token. This invokes custom AccountAuthenticator which makes a call to get new refresh token.
accessToken = accountManager.blockingGetAuthToken(account, authTokenType, false);
if (accessToken != null) {
Request.Builder requestBuilder = response.request().newBuilder();
// Add headers with new refreshToken
return requestBuilder.build();
} catch (Throwable t) {
Timber.e(t, t.getLocalizedMessage());
}
}
return null;
}
}
これに似たいくつかの質問: OkHttp and Retrofit、refresh token with concurrent requests
accountManager.blockingGetAuthToken
(または非ブロッキングバージョン)は、インターセプター以外の場所で呼び出すことができます。したがって、この問題が発生するのを防ぐ正しい場所は、認証子内のです。
アクセストークンを必要とする最初のスレッドがそれを取得することを確認し、最初のスレッドがトークンの取得を完了したときに呼び出されるコールバックに他のスレッドを登録する必要があります。
朗報は、AbstractAccountAuthenticator
には既に非同期結果、つまりAccountAuthenticatorResponse
を提供する方法があり、onResult
またはonError
。
次のサンプルは3つのブロックで構成されています。
first1つは、1つのスレッドのみがアクセストークンをフェッチし、他のスレッドはコールバック用にresponse
を登録するだけです。
second部分は単なる空の結果バンドルです。ここで、トークンをロードしたり、場合によっては更新したりします。
thirdの部分は、結果(またはエラー)を取得した後に行うことです。登録されている可能性のある他のすべてのスレッドの応答を必ず呼び出す必要があります。
boolean fetchingToken;
List<AccountAuthenticatorResponse> queue = null;
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
synchronized (this) {
if (fetchingToken) {
// another thread is already working on it, register for callback
List<AccountAuthenticatorResponse> q = queue;
if (q == null) {
q = new ArrayList<>();
queue = q;
}
q.add(response);
// we return null, the result will be sent with the `response`
return null;
}
// we have to fetch the token, and return the result other threads
fetchingToken = true;
}
// load access token, refresh with refresh token, whatever
// ... todo ...
Bundle result = Bundle.EMPTY;
// loop to make sure we don't drop any responses
for ( ; ; ) {
List<AccountAuthenticatorResponse> q;
synchronized (this) {
// get list with responses waiting for result
q = queue;
if (q == null) {
fetchingToken = false;
// we're done, nobody is waiting for a response, return
return null;
}
queue = null;
}
// inform other threads about the result
for (AccountAuthenticatorResponse r : q) {
r.onResult(result); // return result
}
// repeat for the case another thread registered for callback
// while we were busy calling others
}
}
null
を使用する場合は、必ずすべてのパスでresponse
を返すようにしてください。
別の応答で@matrixが示すアトミックなど、他の手段を使用してこれらのコードブロックを同期することは明らかです。 synchronized
を利用しました。これは実装を把握するのが最も簡単だと信じているからです。
上記のサンプルは、 ここで説明するエミッターループ の適合バージョンで、同時実行性について詳しく説明しています。 RxJavaが内部でどのように機能するかに興味がある場合、このブログは素晴らしい情報源です。
あなたはこれを行うことができます:
それらをデータメンバーとして追加します。
// these two static variables serve for the pattern to refresh a token
private final static ConditionVariable LOCK = new ConditionVariable(true);
private static final AtomicBoolean mIsRefreshing = new AtomicBoolean(false);
そして、インターセプトメソッドで:
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Request request = chain.request();
// 1. sign this request
....
// 2. proceed with the request
Response response = chain.proceed(request);
// 3. check the response: have we got a 401?
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
if (!TextUtils.isEmpty(token)) {
/*
* Because we send out multiple HTTP requests in parallel, they might all list a 401 at the same time.
* Only one of them should refresh the token, because otherwise we'd refresh the same token multiple times
* and that is bad. Therefore we have these two static objects, a ConditionVariable and a boolean. The
* first thread that gets here closes the ConditionVariable and changes the boolean flag.
*/
if (mIsRefreshing.compareAndSet(false, true)) {
LOCK.close();
/* we're the first here. let's refresh this token.
* it looks like our token isn't valid anymore.
* REFRESH the actual token here
*/
LOCK.open();
mIsRefreshing.set(false);
} else {
// Another thread is refreshing the token for us, let's wait for it.
boolean conditionOpened = LOCK.block(REFRESH_WAIT_TIMEOUT);
// If the next check is false, it means that the timeout expired, that is - the refresh
// stuff has failed.
if (conditionOpened) {
// another thread has refreshed this for us! thanks!
// sign the request with the new token and proceed
// return the outcome of the newly signed request
response = chain.proceed(newRequest);
}
}
}
}
// check if still unauthorized (i.e. refresh failed)
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
... // clean your access token and Prompt for request again.
}
// returning the response to the original request
return response;
}
この方法では、トークンを更新するためのリクエストを1つだけ送信し、それ以外の場合は更新されたトークンを取得します。
このアプリケーションレベルのインターセプターで試すことができます
private class HttpInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
//Build new request
Request.Builder builder = request.newBuilder();
builder.header("Accept", "application/json"); //if necessary, say to consume JSON
String token = settings.getAccessToken(); //save token of this request for future
setAuthHeader(builder, token); //write current token to request
request = builder.build(); //overwrite old request
Response response = chain.proceed(request); //perform request, here original request will be executed
if (response.code() == 401) { //if unauthorized
synchronized (httpClient) { //perform all 401 in sync blocks, to avoid multiply token updates
String currentToken = settings.getAccessToken(); //get currently stored token
if(currentToken != null && currentToken.equals(token)) { //compare current token with token that was stored before, if it was not updated - do update
int code = refreshToken() / 100; //refresh token
if(code != 2) { //if refresh token failed for some reason
if(code == 4) //only if response is 400, 500 might mean that token was not updated
logout(); //go to login screen
return response; //if token refresh failed - show error to user
}
}
if(settings.getAccessToken() != null) { //retry requires new auth token,
setAuthHeader(builder, settings.getAccessToken()); //set auth token to updated
request = builder.build();
return chain.proceed(request); //repeat request with new token
}
}
}
return response;
}
private void setAuthHeader(Request.Builder builder, String token) {
if (token != null) //Add Auth token to each request if authorized
builder.header("Authorization", String.format("Bearer %s", token));
}
private int refreshToken() {
//Refresh token, synchronously, save it, and return result code
//you might use retrofit here
}
private int logout() {
//logout your user
}
}
このようなインターセプターをokHttpインスタンスに設定できます
Gson gson = new GsonBuilder().create();
OkHttpClient httpClient = new OkHttpClient();
httpClient.interceptors().add(new HttpInterceptor());
final RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(BuildConfig.REST_SERVICE_URL)
.setClient(new OkClient(httpClient))
.setConverter(new GsonConverter(gson))
.setLogLevel(RestAdapter.LogLevel.BASIC)
.build();
remoteService = restAdapter.create(RemoteService.class);
お役に立てれば!!!!
私はオーセンティケーターで解決策を見つけました、idはリクエストの番号であり、識別のみを目的としています。コメントはスペイン語です
private final static Lock locks = new ReentrantLock();
httpClient.authenticator(new Authenticator() {
@Override
public Request authenticate(@NonNull Route route,@NonNull Response response) throws IOException {
Log.e("Error" , "Se encontro un 401 no autorizado y soy el numero : " + id);
//Obteniendo token de DB
SharedPreferences prefs = mContext.getSharedPreferences(
BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
String token_db = prefs.getString("refresh_token","");
//Comparando tokens
if(mToken.getRefreshToken().equals(token_db)){
locks.lock();
try{
//Obteniendo token de DB
prefs = mContext.getSharedPreferences(
BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
String token_db2 = prefs.getString("refresh_token","");
//Comparando tokens
if(mToken.getRefreshToken().equals(token_db2)){
//Refresh token
APIClient tokenClient = createService(APIClient.class);
Call<AccessToken> call = tokenClient.getRefreshAccessToken(API_OAUTH_CLIENTID,API_OAUTH_CLIENTSECRET, "refresh_token", mToken.getRefreshToken());
retrofit2.Response<AccessToken> res = call.execute();
AccessToken newToken = res.body();
// do we have an access token to refresh?
if(newToken!=null && res.isSuccessful()){
String refreshToken = newToken.getRefreshToken();
Log.e("Entra", "Token actualizado y soy el numero : " + id + " : " + refreshToken);
prefs = mContext.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
prefs.edit().putBoolean("log_in", true).apply();
prefs.edit().putString("access_token", newToken.getAccessToken()).apply();
prefs.edit().putString("refresh_token", refreshToken).apply();
prefs.edit().putString("token_type", newToken.getTokenType()).apply();
locks.unlock();
return response.request().newBuilder()
.header("Authorization", newToken.getTokenType() + " " + newToken.getAccessToken())
.build();
}else{
//Dirigir a login
Log.e("redirigir", "DIRIGIENDO LOGOUT");
locks.unlock();
return null;
}
}else{
//Ya se actualizo tokens
Log.e("Entra", "El token se actualizo anteriormente, y soy el no : " + id );
prefs = mContext.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
String type = prefs.getString("token_type","");
String access = prefs.getString("access_token","");
locks.unlock();
return response.request().newBuilder()
.header("Authorization", type + " " + access)
.build();
}
}catch (Exception e){
locks.unlock();
e.printStackTrace();
return null;
}
}
return null;
}
});