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?


当前回答

您可以尝试为所有的加载器创建一个基类,在这个基类中您可以捕获特定的异常,然后根据需要进行操作。 让所有不同的加载器从基类扩展,以传播行为。

其他回答

正如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)

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

请不要使用拦截器来处理身份验证。

目前,处理身份验证的最佳方法是使用新的Authenticator API,它是专门为此目的设计的。

当响应为401未授权重试最后一次失败的请求时,OkHttp将自动向验证者请求凭据。

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

将验证器附加到OkHttpClient,方法与拦截器相同

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

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

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

您可以尝试为所有的加载器创建一个基类,在这个基类中您可以捕获特定的异常,然后根据需要进行操作。 让所有不同的加载器从基类扩展,以传播行为。

使用像@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 flow

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

希望能有所帮助