We are using Retrofit in our Android app, to communicate with an OAuth2 secured server. Everything works great, we use the RequestInterceptor to include the access token with each call. However there will be times, when the access token will expire, and the token needs to be refreshed. When the token expires, the next call will return with an Unauthorized HTTP code, so that's easy to monitor. We could modify each Retrofit call the following way: In the failure callback, check for the error code, if it equals Unauthorized, refresh the OAuth token, then repeat the Retrofit call. However, for this, all calls should be modified, which is not an easily maintainable, and good solution. Is there a way to do this without modifying all Retrofit calls?
当前回答
我知道这是一条老帖子,但以防有人无意中发现。
TokenAuthenticator依赖于服务类。服务类依赖于OkHttpClient实例。要创建OkHttpClient,我需要TokenAuthenticator。我该如何打破这个循环?两个不同的OkHttpClients?它们将有不同的连接池..
我也面临着同样的问题,但我想只创建一个OkHttpClient因为我不认为我需要另一个TokenAuthenticator本身,我用Dagger2,所以我最终提供服务类TokenAuthenticator懒惰的注入,你可以阅读更多关于懒惰匕首2中注入,但基本上就像说匕首TokenAuthenticator所需的不去创建服务。
你可以参考这个SO线程的示例代码:如何解决一个循环依赖,同时仍然使用Dagger2?
其他回答
我知道这是一条老帖子,但以防有人无意中发现。
TokenAuthenticator依赖于服务类。服务类依赖于OkHttpClient实例。要创建OkHttpClient,我需要TokenAuthenticator。我该如何打破这个循环?两个不同的OkHttpClients?它们将有不同的连接池..
我也面临着同样的问题,但我想只创建一个OkHttpClient因为我不认为我需要另一个TokenAuthenticator本身,我用Dagger2,所以我最终提供服务类TokenAuthenticator懒惰的注入,你可以阅读更多关于懒惰匕首2中注入,但基本上就像说匕首TokenAuthenticator所需的不去创建服务。
你可以参考这个SO线程的示例代码:如何解决一个循环依赖,同时仍然使用Dagger2?
经过长时间的研究,我自定义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;
}
}
正如Brais Gabin在评论中所说,我遇到了TokenAuthenticator依赖于服务类的问题。服务类依赖于OkHttpClient实例,要创建OkHttpClient,我需要TokenAuthenticator。
那么我是如何打破这个循环的呢?
我创建了一个新的okHttpClient对象,一个新的Retrofit对象,并使用该对象调用refreshToken来获得新的令牌(检查getUpdatedToken()函数)
class TokenAuthenticator : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
return runBlocking {
// 1. Refresh your access_token using a synchronous api request
val response = getUpdatedToken(refreshToken)
//2. In my case here I store the new token and refreshToken into SharedPreferences
response.request.newBuilder()
.header("Authorization", "Bearer ${tokenResponse.data?.accessToken}")
.build()
// 3. If there's any kind of error I return null
}
}
private suspend fun getUpdatedToken( refreshToken: String): TokenResponse {
val okHttpClient = OkHttpClient().newBuilder()
.addInterceptor(errorResponseInterceptor)
.build()
val retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.build()
val service = retrofit.create(RefreshTokenApi::class.java)
return service.refreshToken(refreshToken)
}
}
RefreshTokenApi
interface RefreshTokenApi {
@FormUrlEncoded
@POST("refreshToken")
suspend fun refreshToken(
@Field("refresh_token") refreshToeken: String
): TokenResponse
}
在这个项目中,我使用Koin,我这样配置:
object RetrofigConfig {
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
fun provideOkHttpClient(
tokenAuthenticator: TokenAuthenticator
): OkHttpClient {
return OkHttpClient().newBuilder()
.authenticator(tokenAuthenticator)
.build()
}
fun provideServiceApi(retrofit: Retrofit): ServiceApi {
return retrofit.create(ServiceApi::class.java)
}
}
重要的一行是OkHttpClient().newBuilder().authenticator(tokenAuthenticator)
因为这是我第一次实现这个,我不知道这是否是最好的方式,但这是它在我的项目中工作的方式。
使用像@theblang answer这样的TokenAuthenticator是处理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<AccessToken>
@POST("refresh_token")
@FormUrlEncoded
fun refreshToken(@Field("refresh_token") refreshToken: String): Single<AccessToken>
}
and
interface PotoAuthApi { // Authentication API
@GET("api/images")
fun getImage(): Single<GetImageResponse>
}
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())
}
}
最后,在创建服务PotoAuthApi时向OKHttpClient添加拦截器和验证器
Demo
https://github.com/PhanVanLinh/AndroidMVPKotlin
Note
Authenticator flowExample API getImage() return 401 error code authenticate method inside TokenAuthenticator will fired Synchronize noneAuthAPI.refreshToken(...) called After noneAuthAPI.refreshToken(...) response -> new token will add to header getImage() will AUTO called with new header (HttpLogging WILL NOT log this call) (intercept inside AuthInterceptor WILL NOT CALLED) If getImage() still failed with error 401, authenticate method inside TokenAuthenticator will fired AGAIN and AGAIN then it will throw error about call method many time(java.net.ProtocolException: Too many follow-up requests). You can prevent it by count response. Example, if you return null in authenticate after 3 times retry, getImage() will finish and return response 401 If getImage() response success => we will result the result normally (like you call getImage() with no error)
希望能有所帮助
如果你正在使用Retrofit >= 1.9.0,那么你可以使用OkHttp的新拦截器,它是在OkHttp 2.2.0中引入的。您可能希望使用Application 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) {
// close previous response
response.close()
// 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;
}
}
定义拦截器之后,创建OkHttpClient并将拦截器添加为应用程序拦截器。
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.interceptors().add(new CustomInterceptor());
最后,在创建RestAdapter时使用这个OkHttpClient。
RestService restService = new RestAdapter().Builder
...
.setClient(new OkClient(okHttpClient))
.create(RestService.class);
警告:正如杰西威尔逊(来自Square)在这里提到的,这是一个危险的电量。
话虽如此,我绝对认为这是现在处理这种事情的最好方法。如果你有任何问题,请不要犹豫在评论中提问。
推荐文章
- 警告:API ' variable . getjavacompile()'已过时,已被' variable . getjavacompileprovider()'取代
- 安装APK时出现错误
- 碎片中的onCreateOptionsMenu
- TextView粗体通过XML文件?
- 如何使线性布局的孩子之间的空间?
- DSL元素android.dataBinding。enabled'已过时,已被'android.buildFeatures.dataBinding'取代
- ConstraintLayout:以编程方式更改约束
- PANIC: AVD系统路径损坏。检查ANDROID_SDK_ROOT值
- 如何生成字符串类型的buildConfigField
- Recyclerview不调用onCreateViewHolder
- Android API 21工具栏填充
- Android L中不支持操作栏导航模式
- 如何在TextView中添加一个子弹符号?
- PreferenceManager getDefaultSharedPreferences在Android Q中已弃用
- 在Android Studio中创建aar文件