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

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

Не используйте Interceptors для проверки подлинности.

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

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

Используйте этот клиент при создании своего RestAdapter

 RestAdapter restAdapter = new RestAdapter.Builder() .setEndpoint(ENDPOINT) .setClient(new OkClient(okHttpClient)) .build(); return restAdapter.create(API.class); 

Если вы используете Retrofit > = 1.9.0 тогда вы можете использовать новый Interceptor OkHttp , который был введен в OkHttp 2.2.0 . Вы бы хотели использовать Interceptor Application , который позволяет retry and make multiple calls .

Ваш Interceptor мог бы выглядеть примерно так, как этот псевдокод:

 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 и добавьте перехватчик в качестве перехватчика приложений .

  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 (с площади), это опасная сила.

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

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

Если у вас есть, скажем, Retrofit TokenService который вам нужен в вашем Authenticator но вам нужно будет только настроить один OkHttpClient вы можете использовать TokenServiceHolder в качестве зависимости для TokenAuthenticator . Вам нужно будет поддерживать ссылку на него на уровне приложения (singleton). Это легко, если вы используете Dagger 2, иначе просто создайте поле classа внутри приложения.

В 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's 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 или аналогичную схему инъекций зависимостей, есть несколько примеров ответов на этот вопрос

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

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

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

 restAdapter = new RestAdapter.Builder() .setEndpoint(SERVER_END_POINT) .setClient(new CookiePersistingClient()) .setLogLevel(RestAdapter.LogLevel.FULL).build(); 

Cookie Постоянный клиент, который поддерживает cookies для всех запросов и проверок с каждым ответом на запрос, если это несанкционированный доступ 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's 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 accessTokenCallback = new Callback() { @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; } } 

Каждому, кто хотел решить параллельные / параллельные вызовы при обновлении токена. Вот обходной путь

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

Я знаю эту старую нить, но на всякий случай кто-то наткнулся на нее.

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

Я столкнулся с одной и той же проблемой, но мне захотелось создать только один OkHttpClient, потому что я не думаю, что мне нужен другой только для самого TokenAuthenticator, я использовал Dagger2, поэтому я закончил тем, что предоставлял class обслуживания, как Lazy, введенный в TokenAuthenticator, вы можете прочитать больше о ленивой инъекции в кинжале 2 здесь , но это похоже на то, что в основном говорит Кинжал, чтобы НЕ идти и немедленно создавать службу, необходимую TokenAuthenticator.

Вы можете обратиться к этому streamу SO для примера кода: как разрешить циклическую зависимость при использовании Dagger2?

Использование TokenAuthenticator как ответ @theblang, является правильным способом для handle refresh_token .

Вот мой инструмент (у меня есть Kotlin, Dagger, RX, но вы можете использовать эту идею для реализации в своем случае)
TokenAuthenticator

 class TokenAuthenticator @Inject constructor(private val noneAuthAPI: PotoNoneAuthApi, private val accessTokenWrapper: AccessTokenWrapper) : Authenticator { override fun authenticate(route: Route, response: Response): Request? { val newAccessToken = noneAuthAPI.refreshToken(accessTokenWrapper.getAccessToken()!!.refreshToken).blockingGet() accessTokenWrapper.saveAccessToken(newAccessToken) // save new access_token for next called return response.request().newBuilder() .header("Authorization", newAccessToken.token) // just only need to override "Authorization" header, don't need to override all header since this new request is create base on old request .build() } } 

Для предотвращения цикла зависимостей, такого как комментарий @Brais Gabin, я создаю 2 интерфейса, например

 interface PotoNoneAuthApi { // NONE authentication API @POST("/login") fun login(@Body request: LoginRequest): Single @POST("refresh_token") @FormUrlEncoded fun refreshToken(@Field("refresh_token") refreshToken: String): Single } 

а также

 interface PotoAuthApi { // Authentication API @GET("api/images") fun getImage(): Single } 

Класс AccessTokenWrapper

 class AccessTokenWrapper constructor(private val sharedPrefApi: SharedPrefApi) { private var accessToken: AccessToken? = null // get accessToken from cache or from SharePreference fun getAccessToken(): AccessToken? { if (accessToken == null) { accessToken = sharedPrefApi.getObject(SharedPrefApi.ACCESS_TOKEN, AccessToken::class.java) } return accessToken } // save accessToken to SharePreference fun saveAccessToken(accessToken: AccessToken) { this.accessToken = accessToken sharedPrefApi.putObject(SharedPrefApi.ACCESS_TOKEN, accessToken) } } 

Класс AccessToken

 data class AccessToken( @Expose var token: String, @Expose var refreshToken: String) 

Мой перехватчик

 class AuthInterceptor @Inject constructor(private val accessTokenWrapper: AccessTokenWrapper): Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() val authorisedRequestBuilder = originalRequest.newBuilder() .addHeader("Authorization", accessTokenWrapper.getAccessToken()!!.token) .header("Accept", "application/json") return chain.proceed(authorisedRequestBuilder.build()) } } 

Наконец, добавьте Interceptor и Authenticator в ваш OKHttpClient при создании сервиса PotoAuthApi

демонстрация

https://github.com/PhanVanLinh/AndroidMVPKotlin

Заметка

Поток аутентификатора

  • Пример API getImage() возвращает код ошибки 401
  • метод authenticate внутри TokenAuthenticator будет запущен
  • Синхронизировать noneAuthAPI.refreshToken(...) вызванный
  • После noneAuthAPI.refreshToken(...) -> новый токен добавит в заголовок
  • getImage() будет вызывать AUTO с новым заголовком ( HttpLogging НЕ регистрирует этот вызов) ( intercept внутри AuthInterceptor НЕ НАЗЫВАЕТ )
  • Если getImage() все еще не удалось с ошибкой 401, метод authenticate внутри TokenAuthenticator будет запущен TokenAuthenticator и TokenAuthenticator , тогда он будет многократно вызывать ошибку о методе вызова. Вы можете предотвратить его по счету ответа . Например, если вы return null в authenticate после 3-х повторных попыток, getImage() завершит и return response 401

  • Если getImage() response success => мы получим результат (как вы называете getImage() без ошибок)

Надеюсь, что это поможет

Interesting Posts

Возможно ли гибридное USB-Stick USB для UEFI и устаревшего BIOS?

Почему я не могу создать массив с размером, определяемым глобальной переменной?

SSH аутентификация с открытым ключом не выполняется

динамически объявлять бобы во время выполнения весной

заполнять дерево из списка путей файла в wpf

Что такое сборки .NET?

«Все параметры загрузки проверяются» после обновления BIOS

Автоматическое выключение с помощью Angularjs на основе пользователя бездействия

XML-схема minOccurs / maxOccurs значения по умолчанию

Не удается получить доступ к ресурсам LAN при подключении через беспроводную сеть

Метод getContactsFromFirebase () возвращает пустой список

В чем разница между Thread start () и Runnable run ()

Как разбить файл MP3 без повторного кодирования?

Как создать пользовательскую функцию EL для вызова статического метода?

Как заставить Windows перестать предполагать приоритет над GRUB?

Давайте будем гением компьютера.