使用Retrofit刷新OAuth令牌,而不修改所有调用

我们在我们的Android应用程序中使用Retrofit,与OAuth2安全服务器通信。一切都很好,我们使用RequestInterceptor在每个调用中包含访问令牌。 但是,有时访问令牌会过期,需要刷新令牌。当令牌过期时,下一个调用将返回一个未授权的HTTP代码,因此很容易监控。 我们可以按照以下方式修改每个Retrofit调用: 在失败回调中,检查错误代码,如果它等于未授权,则刷新OAuth令牌,然后重复Retrofit调用。 然而,为此,应该修改所有调用,这不是一个容易维护的好解决方案。 有没有办法做到这一点而不修改所有的Retrofit调用?< / p >

103056 次浏览
你可以试着为你所有的加载器创建一个基类,在这个基类中你可以捕获一个特定的异常,然后根据你的需要进行操作。 让所有不同的加载器都从基类扩展,以便扩展行为

如果你正在使用改造 >= 1.9.0,那么你可以使用OkHttp的 new 拦截器,它是在OkHttp 2.2.0中引入的。你会想要使用应用程序拦截器,它允许你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) {
// 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;
}


}

定义了Interceptor之后,创建OkHttpClient并将拦截器添加为应用程序拦截器

    OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.interceptors().add(new CustomInterceptor());

最后,在创建你的RestAdapter时使用这个OkHttpClient

    RestService restService = new RestAdapter().Builder
...
.setClient(new OkClient(okHttpClient))
.create(RestService.class);

正如Jesse Wilson (from Square)提到的在这里,这是一个危险的电量。

话虽如此,我绝对认为这是现在处理这种事情的最好方法。如果你有任何问题,请不要犹豫在评论中提问。

经过长时间的研究,我自定义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;
}
}

请不要使用Interceptors来处理身份验证。

目前,处理身份验证的最佳方法是使用新的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;
}

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

TokenAuthenticator依赖于服务类。服务类依赖于OkHttpClient实例。要创建OkHttpClient,我需要TokenAuthenticator。我该如何打破这个循环?两个不同的OkHttpClients?它们将有不同的连接池..

如果你在你的Authenticator中有一个你需要的Retrofit TokenService,但是你只想建立一个OkHttpClient,你可以使用TokenServiceHolder作为TokenAuthenticator的依赖项。您必须在应用程序(单例)级别维护对它的引用。如果你使用匕首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'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);

如果你正在使用匕首2或类似的依赖注入框架,这个问题的答案中有一些例子

给任何人谁想解决并发/并行调用时刷新令牌。这里有一个变通办法

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依赖于服务类。服务类依赖于OkHttpClient实例。要创建OkHttpClient,我需要TokenAuthenticator。我该如何打破这个循环?两个不同的OkHttpClients?它们将有不同的连接池..

我也面临着同样的问题,但我想只创建一个OkHttpClient因为我不认为我需要另一个TokenAuthenticator本身,我用Dagger2,所以我最终提供服务类懒惰的注入 TokenAuthenticator,你可以阅读更多关于懒惰注入在在这里第2匕首,但基本上就像说匕首 TokenAuthenticator所需的去创建服务。

你可以参考这个SO线程的样例代码:如何解决循环依赖,同时仍然使用Dagger2?

像@theblang answer一样使用TokenAuthenticator是句柄refresh_token的正确方式。

这是我的实现(我已经使用Kotlin,匕首,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>
}

而且

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时,将InterceptorAuthenticator添加到你的OKHttpClient

演示

https://github.com/PhanVanLinh/AndroidMVPKotlin

请注意

身份验证流
  • 示例API getImage()返回401错误码
  • TokenAuthenticator中的authenticate方法将解雇了
  • 同步noneAuthAPI.refreshToken(...)调用
  • noneAuthAPI.refreshToken(...) response ->新令牌将添加到头
  • getImage()将带有新头的汽车被称为 (HttpLogging 不会记录日志此调用)(interceptAuthInterceptor 不会被调用内)
  • 如果getImage()仍然失败,错误401,TokenAuthenticator中的authenticate方法将再次发射,然后它将多次抛出关于调用方法的错误(java.net.ProtocolException: Too many follow-up requests)。你可以通过count response来防止它。例如,如果你在authenticatereturn null重试3次后,getImage()finishreturn response 401

  • 如果getImage() response success =>,我们将正常计算结果(就像你调用getImage()没有错误一样)

希望能有所帮助

使用一个拦截器(注入令牌)和一个验证器(刷新操作)完成工作,但是:

我也有一个双重调用的问题:第一个电话总是返回401: 在第一次调用(拦截器)时没有注入令牌,并且调用了验证器:发出了两个请求

修复只是在拦截器中重新影响对构建的请求:

之前:

private Interceptor getInterceptor() {
return (chain) -> {
Request request = chain.request();
//...
request.newBuilder()
.header(AUTHORIZATION, token))
.build();
return chain.proceed(request);
};
}

后:

private Interceptor getInterceptor() {
return (chain) -> {
Request request = chain.request();
//...
request = request.newBuilder()
.header(AUTHORIZATION, token))
.build();
return chain.proceed(request);
};
}

在一个街区:

private Interceptor getInterceptor() {
return (chain) -> {
Request request = chain.request().newBuilder()
.header(AUTHORIZATION, token))
.build();
return chain.proceed(request);
};
}

希望能有所帮助。

编辑:我没有找到一种方法来避免第一次调用总是返回401,只使用验证器而没有拦截器

正如brai盖宾在评论中所说的那样,我遇到了TokenAuthenticator依赖于服务类的问题。服务类依赖于OkHttpClient实例,要创建OkHttpClient实例,我需要TokenAuthenticator

那么我是如何打破这个循环的呢?

我创建了一个新的okHttpClient对象,一个新的Retrofit对象,并使用refreshToken (check 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)
}
}

这里的重要行是.newBuilder OkHttpClient () () .authenticator (tokenAuthenticator)

因为这是我第一次实现这个,我不知道这是否是最好的方式,但这是它在我的项目中工作的方式。