Есть ли способ дождаться обновления токена в перехватчике okhttp3? - PullRequest
2 голосов
/ 25 марта 2019

Недавно мне пришлось работать с большим старым проектом, который использует Retrofit 1, okhttp3, jobManager и Picasso 2.71828

Приложение получает данные с сервера. Логика взаимодействия: пользователь входит в систему, получает токен, токен обновления. Они хранятся в SharedPreferences с помощью shHelper. С помощью токена он может отправлять запросы (где-то в URL, а где-то в теле), с помощью токена обновления пользователь может получить новый токен, если сеанс сброшен или токен гнилой.

Ошибки авторизации (401) обрабатываются аутентификатором okhttp3, который нам удалось использовать с Picasso. Но возникла проблема - Пикассо, если на экране несколько изображений, - отправляет несколько запросов подряд, одновременно или почти одновременно, и, поскольку все они сразу получают ответ 401, если токен гнилой, аутентификатор немедленно отправляет один и тот же номер. запросов на обновление токена. Есть ли какой-нибудь элегантный способ дождаться обновления токена и затем повторить запросы для остальных картинок? Теперь это происходит следующим образом - получив ошибку 401, токен сбрасывается в ноль (token = "") и все другие потоки, попадающие в аутентификатор, проверяют, выполняют ли (token == "") Thread.sleep () и I я очень недоволен этим

private Authenticator getAuthenticator() {
        return (route, response) -> {
            if (errorCount > 3){
                return null;
            }

            if (response.request().url().toString().endsWith("/refreshToken")) {
                Log.d(TAG, "getAuthenticator: " + "refreshToken");
                PasswordRepeatActivity.start(context);
                return null;
            }

            if (response.request().url().toString().endsWith("/auth")) {
                String message = "Попробуйте позже";
                try {
                    com.google.gson.Gson gson = Gson.builder().create();
                    ApiResponse apiError = gson.fromJson(response.body().string(), ApiResponse.class);
                    message = apiError.getMessage();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                throw new IOException(message);
            }

            String login = spHelper.getCurrentLogin();
            Auth auth = spHelper.getAuth(login);
            String token = auth.getToken();
            HttpUrl oldUrl = response.request().url();

            //if token is empty - repeat checking after some time
            Log.d(TAG, "getAuthenticator: token ==" + token);
            if (token != null && token.isEmpty()) {
                boolean isEmpty = true;
                while (isEmpty){
                    try {
                        Log.d(TAG, "Authenticator: sleeping...");
                        Thread.sleep(500);

                        String mToken = spHelper.getAuth(login).getToken();
                        if (mToken!= null && !mToken.isEmpty()){
                            isEmpty = false;
                        }
                        Log.d(TAG, "Authenticator: check if token is refreshed");
                        if (!mToken.isEmpty() && oldUrl.toString().contains("token") && !mToken.equals(oldUrl.queryParameter("token"))) {
                            Log.d(TAG, "Authenticator: token is valid, token: " + mToken);
                            return getRefreshedUrlRequest(mToken, oldUrl);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        return response.request();
                    }
                }

                return response.request();
            } else if (oldUrl.toString().contains("token") && !token.equals(oldUrl.queryParameter("token"))) {
                Log.d(TAG, "Authenticator: token is valid, token: " + token);
                return getRefreshedUrlRequest(token, oldUrl);
            } else {

                auth.clearToken();
                spHelper.putAuth(login, auth);

                String refreshToken = auth.getRefreshToken();
                RefreshRequest refreshRequest = new RefreshRequest(refreshToken);
                try {
                    AuthResponse refreshResponse = dataApi.refresh(refreshRequest);
                    errorCount = 0;
                    Auth newAuth = refreshResponse.getResponse();
                    spHelper.putAuth(login, newAuth);

                    Request request = response.request();
                    RequestBody requestBody = request.body();
                    String newToken = newAuth.getToken();

                    Log.d(TAG, "Authenticator: token refreshed, old token: " + token + " -> " + "new token : " + newToken);
                    if (oldUrl.toString().contains("token")) {
                        return getRefreshedUrlRequest(newToken, oldUrl);
                    }
                    if (requestBody != null
                            && requestBody.contentType() != null
                            && requestBody.contentType().subtype() != null
                            && requestBody.contentType().subtype().contains("json")) {
                        requestBody = processApplicationJsonRequestBody(requestBody, newToken);
                    }
                    if (requestBody != null) {
                        Request.Builder requestBuilder = request.newBuilder();
                        request = requestBuilder
                                .post(requestBody)
                                .build();
                    } else {
                        LoginActivity.show(context);
                    }
                    return request;
                } catch (RequestException e) {
                    AtlasPatienteLog.d(TAG, "Can't refresh token: " + e.getMessage());
                    return response.request();
                }
            }
        };
    }

Я ищу способы после первой ошибки 401 отправить один запрос, чтобы обновить токен и дождаться его со всеми другими потоками, а затем отправить запросы с новым токеном. Помимо ожидания обновления токена в аутентификаторе, есть ли способ как-то упростить этот код? Теперь этот метод имеет длину около 100 строк, и каждый раз, когда необходимо изменить его, даже чтение и сохранение логики в вашей голове становится проблемой.

Итак, через некоторое время и некоторые попытки я сделал часть аутентификатора синхронизированной с некоторым объектом блокировки. Теперь только один поток одновременно может получить доступ к аутентификатору. Итак, если токену нужно обновить bs - он будет, и после обновления все потоки, ожидающие новый токен, будут повторять свои вызовы с новым токеном. Спасибо @Yuri Schimke за обмен очень полезной информацией.

private Authenticator getAuthenticator() {
        return (route, response) -> {
            String responseUrl = response.request().url().toString();
            if (responseUrl.endsWith("/refreshToken") ) {
                Log.d(TAG, "getAuthenticator: " + "refreshToken");
                PasswordRepeatActivity.start(context);
                return null;
            }
            if (responseUrl.endsWith("/auth")) {
                String message = "Попробуйте позже";
                try {
                    com.google.gson.Gson gson = Gson.builder().create();
                    ApiResponse apiError = gson.fromJson(response.body().string(), ApiResponse.class);
                    message = apiError.getMessage();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                throw new IOException(message);
            }
            synchronized (LOCK) {
                String login = spHelper.getCurrentLogin();
                Auth auth = spHelper.getAuth(login);
                String token = auth.getToken();
                HttpUrl oldUrl = response.request().url();

                if (oldUrl.toString().contains("token") && !token.equals(oldUrl.queryParameter("token"))) {
                    Log.d(TAG, "Authenticator: token is valid, token: " + token);
                    return getRefreshedUrlRequest(token, oldUrl);
                } else {
                    String refreshToken = auth.getRefreshToken();
                    RefreshRequest refreshRequest = new RefreshRequest(refreshToken);
                    try {
                        AuthResponse refreshResponse = dataApi.refresh(refreshRequest);
                        Auth newAuth = refreshResponse.getResponse();
                        spHelper.putAuth(login, newAuth);

                        Request request = response.request();
                        RequestBody requestBody = request.body();
                        String newToken = newAuth.getToken();

                        Log.d(TAG, "Authenticator: token refreshed, old token: " + token + " -> " + "new token : " + newToken);
                        if (oldUrl.toString().contains("token")) {
                            return getRefreshedUrlRequest(newToken, oldUrl);
                        }
                        if (requestBody != null
                                && requestBody.contentType() != null
                                && requestBody.contentType().subtype() != null
                                && requestBody.contentType().subtype().contains("json")) {
                            requestBody = processApplicationJsonRequestBody(requestBody, newToken);
                        }
                        if (requestBody != null) {
                            Request.Builder requestBuilder = request.newBuilder();
                            request = requestBuilder
                                    .post(requestBody)
                                    .build();
                        } else {
                            LoginActivity.show(context);
                        }
                        return request;
                    } catch (RequestException e) {
                        AtlasPatienteLog.d(TAG, "Can't refresh token: " + e.getMessage());
                        PasswordRepeatActivity.start(context);
                        return null;
                    }
                }
            }
        };
    }

1 Ответ

0 голосов
/ 25 марта 2019

С помощью только OkHttp вам, как правило, придется справляться с этой сложностью в вашем приложении, вне зависимости от того, находится ли оно вне вызова, в Authenticator или внутри упреждающей аутентификации Interceptor.В этих случаях параллелизм также не обрабатывается для вас.

Обсуждается здесь

https://github.com/square/okhttp/issues/3714#issuecomment-350469364

Убедитесь, что вы сделали синхронный вызов обновления, поскольку асинхронный вызов не можетиметь свободный поток для выполнения.

ответ от @swankjesse был таков: если вы делаете синхронизирующий вызов в перехватчике, то вы связываете поток, но не блокируете тупик, потому что он нене нужно захватывать другой поток и не удерживать блокировку в течение этого времени.

Некоторые блоги на подобные темы

https://objectpartners.com/2018/06/08/okhttp-authenticator-selectively-reauthorizing-requests/

https://medium.com/@sandeeptengale/problem-solved-2-access-token-refresh-with-okhttp-authenticator-5ccb798ede70

https://blog.coinbase.com/okhttp-oauth-token-refreshes-b598f55dd3b2

...