Подтвердить что ты не робот

Обновление токена OAuth с использованием Retrofit без изменения всех вызовов

Мы используем Retrofit в нашем Android-приложении для связи с защищенным сервером OAuth2. Все работает отлично, мы используем RequestInterceptor для включения токена доступа с каждым вызовом. Однако будут моменты, когда токен доступа истечет, и токен необходимо обновить. Когда токен истекает, следующий вызов будет возвращаться с помощью неавторизованного HTTP-кода, чтобы его можно было легко контролировать. Мы можем изменить каждый запрос на доработку следующим образом: В обратном вызове сбоя проверьте код ошибки, если он равен Unauthorized, обновите токен OAuth, затем повторите попытку переоснащения. Тем не менее, для этого все вызовы должны быть изменены, что не является легкоподдерживаемым и хорошим решением. Есть ли способ сделать это без изменения всех повторных настроек?

4b9b3361

Ответ 1

Пожалуйста, не используйте Interceptors для проверки подлинности.

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

OkHttp будет автоматически запрашивать Authenticator для учетных данных, когда ответ 401 Not Authorised повторяет последний сбой запроса.

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

Ответ 2

Если вы используете Retrofit >= 1.9.0, тогда вы можете использовать OkHttp новый 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) упоминает здесь, это опасное количество энергии.

С учетом сказанного, я определенно думаю, что это лучший способ справиться с чем-то подобным сейчас. Если у вас есть какие-либо вопросы, пожалуйста, не стесняйтесь спрашивать в комментариях.

Ответ 3

TokenAuthenticator зависит от класса обслуживания. Класс службы зависит от экземпляра OkHttpClient. Чтобы создать OkHttpClient, мне нужен TokenAuthenticator. Как я могу разбить этот цикл? Два разных OkHttpClients? Они будут иметь разные пулы соединений.

Если у вас есть, скажем, Retrofit TokenService, который вам нужен в вашем Authenticator, но вы бы хотели настроить только один OkHttpClient, вы можете использовать TokenServiceHolder в качестве зависимости для TokenAuthenticator. Вам нужно будет поддерживать ссылку на него на уровне приложения (singleton). Это легко, если вы используете 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 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 или аналогичную инфраструктуру инъекций зависимостей, в ответах на этот вопрос есть несколько примеров

Ответ 4

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

Ответ 5

После длительных исследований я настроил клиент Apache для обработки Refreshing AccessToken For Retrofit, в котором вы отправляете токен доступа в качестве параметра.

Инициируйте свой адаптер с постоянным клиентом cookie

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

Ответ 6

Для тех, кто хочет решать параллельные/параллельные вызовы при обновлении токена. Здесь обходной путь

class TokenAuthenticator: Authenticator {

    override fun authenticate(route: Route?, response: Response?): Request? {
        response?.let {
            if (response.code() == 401) {
                while (true) {
                    if (!isRefreshing) {
                        val requestToken = response.request().header(AuthorisationInterceptor.AUTHORISATION)
                        val currentToken = OkHttpUtil.headerBuilder(UserService.instance.token)

                        currentToken?.let {
                            if (requestToken != currentToken) {
                                return generateRequest(response, currentToken)
                            }
                        }

                        val token = refreshToken()
                        token?.let {
                            return generateRequest(response, token)
                        }
                    }
                }
            }
        }

        return null
    }

    private fun generateRequest(response: Response, token: String): Request? {
        return response.request().newBuilder()
                .header(AuthorisationInterceptor.USER_AGENT, OkHttpUtil.UA)
                .header(AuthorisationInterceptor.AUTHORISATION, token)
                .build()
    }

    private fun refreshToken(): String? {
        synchronized(TokenAuthenticator::class.java) {
            UserService.instance.token?.let {
                isRefreshing = true

                val call = ApiHelper.refreshToken()
                val token = call.execute().body()
                UserService.instance.setToken(token, false)

                isRefreshing = false

                return OkHttpUtil.headerBuilder(token)
            }
        }

        return null
    }

    companion object {
        var isRefreshing = false
    }
}