Okhttp 刷新过期令牌时向服务器发送多个请求

2022-09-01 02:10:13

我有一个和三个Web服务调用是在同时加载时进行的。ViewPagerViewPager

当第一个返回401时,被调用并且我刷新里面的令牌,但剩余的2个请求已经使用旧的刷新令牌发送到服务器,并且失败,498在Interceptor中捕获并且应用程序被注销。AuthenticatorAuthenticator

这不是我所期望的理想行为。我想将第 2 个和第 3 个请求保留在队列中,并在刷新令牌时重试排队的请求。

目前,我有一个变量来指示令牌刷新是否正在进行中,在这种情况下,我取消了用户中的所有后续请求,并且用户必须手动刷新页面,或者我可以注销用户并强制用户登录。AuthenticatorInterceptor

对于使用 okhttp 3.x for Android 解决上述问题,什么是好的解决方案或架构?

编辑:我想解决的问题是一般的,我不想对我的电话进行排序。即,等待一个调用完成并刷新令牌,然后仅在活动和片段级别发送请求的其余部分。

已请求代码。这是以下各项的标准代码: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 和 Retrofit,使用并发请求刷新令牌


答案 1

重要的是要注意,(或非阻塞版本)仍然可以在其他地方调用,而不是拦截器。因此,防止此问题发生的正确位置是在身份验证器accountManager.blockingGetAuthToken

我们希望确保需要访问令牌的第一个线程将检索它,并且可能的其他线程应该只注册在第一个线程完成检索令牌时调用回调。
好消息是,它已经有一种提供异步结果的方法,即,您可以在其上调用 或 。AbstractAccountAuthenticatorAccountAuthenticatorResponseonResultonError


下面的示例由 3 个块组成。

一个是关于确保只有一个线程获取访问令牌,而其他线程只是注册回调。response

部分只是一个虚拟的空结果包。在这里,您将加载令牌,可能刷新它等。

部分是一旦你有了结果(或错误),你就会做什么。您必须确保为可能已注册的所有其他线程调用响应。

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

只需确保在使用 时返回所有路径即可。nullresponse

显然,您可以使用其他方法来同步这些代码块,例如在另一个响应中@matrix所示的原子。我使用了 ,因为我相信这是最容易掌握的实现,因为这是一个很好的问题,每个人都应该这样做;)synchronized


上面的示例是此处描述的发射器环路的改编版本,其中详细介绍了并发性。如果你对RxJava如何在引擎盖下工作感兴趣,这个博客是一个很好的来源。


答案 2

您可以执行以下操作:

将它们添加为数据成员:

// 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 个请求来刷新令牌,然后每隔一个请求,您就会拥有刷新的令牌。


推荐