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

时间:2014-03-17 08:52:01

标签: android oauth-2.0 retrofit

我们在Android应用中使用Retrofit,与OAuth2安全服务器进行通信。一切都很好,我们使用RequestInterceptor在每次调用时包含访问令牌。 但是,有时候,访问令牌将过期,并且需要刷新令牌。当令牌过期时,下一个调用将返回一个未授权的HTTP代码,因此很容易监控。 我们可以通过以下方式修改每个Retrofit调用: 在故障回调中,检查错误代码,如果它等于Unauthorized,则刷新OAuth令牌,然后重复Retrofit调用。 但是,为此,应修改所有呼叫,这不是一个易于维护的好解决方案。 有没有办法在不修改所有Retrofit调用的情况下执行此操作?

10 个答案:

答案 0 :(得分:186)

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

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

当回复Authenticator 重试上次失败的请求时,OkHttp将自动询问 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);

在创建Retrofit RestAdapter

时使用此客户端
RestAdapter restAdapter = new RestAdapter.Builder()
                .setEndpoint(ENDPOINT)
                .setClient(new OkClient(okHttpClient))
                .build();
return restAdapter.create(API.class);

答案 1 :(得分:60)

如果您使用Retrofit> = 1.9.0,那么您可以使用OkHttp 2.2.0中引入的OkHttp'sInterceptor。您可以使用Application 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后,创建OkHttpClient并将拦截器添加为Application Interceptor

    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(来自Square)提及here,这是一种危险的权力。

话虽如此,我绝对认为这是现在处理这类事情的最好方法。如果您有任何疑问,请随时发表评论。

答案 2 :(得分:21)

  

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

如果您在TokenService内有所需的改造Authenticator,但您只想设置一个OkHttpClient,则可以使用TokenServiceHolder作为TokenAuthenticator TokenAuthenticator.java的依赖关系。您必须在应用程序(单例)级别维护对它的引用。如果您使用Dagger 2,这很容易,否则只需在您的应用程序中创建类字段。

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

客户端设置:

name

如果您使用的是Dagger 2或类似的依赖注入框架,this question的答案中有一些示例

答案 3 :(得分:2)

像@theblang答案一样使用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()
    }
}

为了防止像依赖周期,我创建了 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错误代码
  • authenticate中的
  • TokenAuthenticator方法将触发
  • 同步称为的noneAuthAPI.refreshToken(...)
  • noneAuthAPI.refreshToken(...)响应之后->新令牌将添加到标头
  • getImage()自动调用并带有新的标题(HttpLogging 将不会记录此呼叫)({{1中的intercept} }} 将不会呼叫
  • 如果AuthInterceptor仍然失败,并显示错误401,getImage()中的authenticate方法将再次触发并再次触发,则它将引发有关调用方法的错误很多次(TokenAuthenticator)。您可以通过count response阻止它。例如,如果您在3次重试后在java.net.ProtocolException: Too many follow-up requestsreturn null,则authenticate完成getImage()

  • 如果return response 401响应成功=>,我们将正常得出结果(就像您正确调用getImage()一样)

希望有帮助

答案 4 :(得分:1)

我知道这是一个旧线程,但以防万一有人偶然发现它。

  

TokenAuthenticator依赖于服务类。服务类取决于OkHttpClient实例。要创建OkHttpClient,我需要TokenAuthenticator。如何打破这个周期?两个不同的OkHttpClients?他们将有不同的连接池。

我遇到了同样的问题,但是我只想创建一个OkHttpClient,因为我不认为只需要为TokenAuthenticator本身提供另一个,我就使用了Dagger2,所以最终我将服务类提供为<在TokenAuthenticator中,strong> Lazy注入,您可以在dagger 2 here中阅读有关Lazy注入的更多信息,但这就像是对Dagger说去创建所需的服务一样由TokenAuthenticator立即提供。

您可以参考此SO线程以获取示例代码:How to resolve a circular dependency while still using Dagger2?

答案 5 :(得分:0)

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

答案 6 :(得分:0)

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

答案 7 :(得分:0)

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

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

解决方法是将请求重新发送到Interceptor中的版本:

之前:

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的方法

答案 8 :(得分:0)

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

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

我创建了一个新的 okHttpClient 对象,一个新的 Retrofit 对象,并使用该对象调用 refreshToken(检查 getUpdatedToken() 函数)以获取新令牌< /p>

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)

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

答案 9 :(得分:-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
    }
}