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

问题描述 投票:27回答:4

当qazxsw poi同时加载时,我有一个qazxsw poi和三个Web服务调用。

当第一个返回401时,调用ViewPager并刷新ViewPager中的令牌,但剩余的2个请求已经发送到具有旧刷新令牌的服务器,并且失败,498在Interceptor中捕获并且app被注销。

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

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

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

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

代码被要求。这是Authenticator的标准代码:

Interceptor

一些类似的问题:Authenticator

java android http retrofit okhttp
4个回答
12
投票

值得注意的是,除了拦截器之外,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,你可以在其上调用AbstractAccountAuthenticatorAccountAuthenticatorResponse


以下示例包含3个块。

第一个是确保只有一个线程获取访问令牌,而其他线程只是注册他们的onResult进行回调。

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

第三部分是你得到结果(或错误)后所做的事情。您必须确保为可能已注册的每个其他线程调用响应。

onError

只需确保在使用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 } }

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


上面的示例是response的改编版本,其中详细介绍了并发性。如果您对RxJava如何在幕后工作感兴趣,这个博客是一个很好的来源。


8
投票

你可以这样做:

将它们添加为数据成员:

synchronized

然后在拦截方法上:

emitter loop described here

通过这种方式,您只需发送1个请求来刷新令牌,然后为每个其他请求刷新令牌。


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);

您可以将此类拦截器设置为okHttp实例

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

希望这可以帮助!!!!


0
投票

我找到了带验证器的解决方案,id是请求的编号,仅用于识别。评论是西班牙语

 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
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.