使用Retrofit刷新OAuth令牌,无需修改所有调用
我们在Android应用中使用Retrofit,与OAuth2安全服务器进行通信。 一切正常,我们使用RequestInterceptor在每个调用中包含访问令牌。 但是,访问令牌将会过期,并且需要刷新令牌。 当令牌到期时,下一次调用将返回一个未经授权的HTTP代码,所以很容易监控。 我们可以按照以下方式修改每个Retrofit调用:在失败callback中,检查错误代码,如果它等于Unauthorized,则刷新OAuth令牌,然后重复Retrofit调用。 但是,为此,所有的调用都应该修改,这不是一个容易维护,很好的解决scheme。 有没有办法做到这一点,而不修改所有的改造呼叫?
请不要使用Interceptors
来处理身份validation。
当前处理身份validation的最佳方法是使用专门为此devise的新的Authenticator
API。
当响应为401 Not Authorised
时,OkHttp会自动向 Authenticator
请求凭证。
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; }
以与Interceptors
相同的方式将Authenticator
附加到OkHttpClient
OkHttpClient okHttpClient = new OkHttpClient(); okHttpClient.setAuthenticator(authAuthenticator);
创buildRetrofit
RestAdapter
时使用此客户端
RestAdapter restAdapter = new RestAdapter.Builder() .setEndpoint(ENDPOINT) .setClient(new OkClient(okHttpClient)) .build(); return restAdapter.create(API.class);
如果你正在使用Retrofit > = 1.9.0
那么你可以使用OkHttp 2.2.0
中引入的OkHttp的新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
,创build一个OkHttpClient
并将拦截器添加为应用程序拦截器 。
OkHttpClient okHttpClient = new OkHttpClient(); okHttpClient.interceptors().add(new CustomInterceptor());
最后,在创buildOkHttpClient
时使用这个RestAdapter
。
RestService restService = new RestAdapter().Builder ... .setClient(new OkClient(okHttpClient)) .create(RestService.class);
警告:正如Jesse Wilson
(来自Square) 在这里提到的,这是一个危险的力量。
有了这个说法,我绝对认为这是现在处理这样的事情的最好办法。 如果您有任何问题,请不要犹豫,以征求意见。
TokenAuthenticator依赖于一个服务类。 服务类依赖于一个OkHttpClient实例。 要创build一个OkHttpClient,我需要TokenAuthenticator。 我怎样才能打破这个循环? 两个不同的OkHttpClient? 他们将有不同的连接池..
如果你有一个在你的Authenticator
需要的Retrofit TokenService
,但是你只想设置一个OkHttpClient
你可以使用一个TokenServiceHolder
作为TokenServiceHolder
的依赖。 你将不得不在应用程序(单身)级别保持对它的引用。 如果使用Dagger 2,这很容易,否则只需在应用程序中创build类字段。
在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或类似的dependency injection框架, 这个问题的答案中有一些例子
您可以尝试为所有的装载机创build一个基类,以便能够捕获特定的exception,然后根据需要进行操作。 使所有不同的装载机从基类扩展到传播行为。
经过长时间的研究,我定制了Apache客户端来处理刷新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'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<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; } }