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

Dan*_*nai 141 android oauth-2.0 retrofit

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

lgv*_*lle 196

请不要Interceptors用于处理身份验证.

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

OkHttp会自动询问Authenticator当响应凭据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;
    }
Run Code Online (Sandbox Code Playgroud)

AuthenticatorOkHttpClient与您相同的方式附加Interceptors

OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setAuthenticator(authAuthenticator);
Run Code Online (Sandbox Code Playgroud)

创建时使用此客户端 Retrofit RestAdapter

RestAdapter restAdapter = new RestAdapter.Builder()
                .setEndpoint(ENDPOINT)
                .setClient(new OkClient(okHttpClient))
                .build();
return restAdapter.create(API.class);
Run Code Online (Sandbox Code Playgroud)

  • `TokenAuthenticator`取决于`service`类.`service`类依赖于`OkHttpClient`实例.要创建一个`OkHttpClient`,我需要`TokenAuthenticator`.我怎么能打破这个循环呢?两个不同的`OkHttpClient`s?他们将拥有不同的连接池...... (48认同)
  • @Jdruwe看起来这个代码会失败1次,然后它会发出请求.但是,如果你添加一个拦截器,其目的只是始终添加一个访问令牌(无论它是否已过期),那么只有在收到401时才会调用它,这只会在该令牌过期时发生. (11认同)
  • 好的,因此解决@ Ihor的问题可能是同步Authenticator中的代码.它解决了我的问题.在Request authenticate(...)方法中: - 执行任何初始化的东西 - 启动同步块(synchronized(MyAuthenticator.class){...}) - 在该块中检索当前访问和刷新令牌 - 检查失败的请求是否使用最新访问令牌(resp.request().header("授权")) - 如果不是仅使用更新的访问令牌再次运行它 - 否则运行刷新令牌流 - 更新/持久更新访问和刷新令牌 - 完成同步块 - 重新运行 (10认同)
  • 许多需要刷新令牌的并行请求怎么样?同时会有很多刷新令牌请求.怎么避免呢? (6认同)
  • 这是否意味着每个请求总是会失败一次,或者在执行请求时添加令牌? (5认同)
  • @Ihor Kostenko在当前的Authenticator行为方面提出了有效的观点.当我们触发多个并行网络请求时,此时access_token将失效,这将导致许多刷新令牌流...在大多数情况下,这将导致用户从已记录状态中退出.第一次刷新会很好,但是下一次刷新将使用最旧的refresh_token,它应该不再有效.我正试图为此找到解决方案:) (3认同)
  • @PaulWoitaschek Paul,不要忘记Authenticator不是该身份验证的唯一部分.你需要有两个部分:拦截器,它注入正确的access_token,然后是Authenticator,以防你没有有效的access_token :)所以你只有在任何网络请求返回401(或407)时才会命中Authenticator.然后,您应该通过运行刷新流来获取新的访问令牌,并且任何进一步的请求都应该没问题,只要具有令牌注入的Interceptor将使用正确/更新的access_token;) (3认同)
  • https://github.com/square/okhttp/wiki/Recipes#handling-authentication这是一个很好的解决方案,它适用于我.但是我没有response.priorResponse().有线索吗? (2认同)
  • 这是一个好主意怎么样?您所做的每一个请求都会被提出两次.进入房间.如果您的头撞到门,请使用钥匙. (2认同)
  • @lgvalle 好吧,如果您与后端通信,您通常需要每个请求都具有 auth 标头。因此,您将直接为每个请求 a 获得 401。如果我错了纠正我!(我很想错;-)) (2认同)
  • 好的,@Ihor 问题的解决方案可能是同步 Authenticator 中的代码。它解决了我的问题: (2认同)
  • 我想了解更多关于service.refreshToken()方法的信息.如何在不崩溃应用程序的情况下同步处理它? (2认同)
  • @nomrasco你能提供有关如何同步验证器的代码吗?如果我有三个请求使用无效令牌启动...我的身份验证器刷新3次. (2认同)
  • 也许这会节省其他人几分钟的时间,但在新版本的 OkHttp 中,添加身份验证器的语法是 `OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder(); okHttpClientBuilder.authenticator(authAuthenticator); OkHttpClient okHttpClient = okHttpClientBuilder.build();` (2认同)
  • 然而,“Authenticator”的操作似乎并不适合这样的情况:您拥有一致的、预定的授权令牌,该令牌的生命周期跨越许多请求(例如 OAuth 令牌或具有任何类型 TTL 的任何令牌)。因此,我在拦截器中完成了它,因为如果全部在“Authenticator”中完成,那么每个请求都会首先响应毫无意义的 401。因此,我的方法(上面也提到过)是让“Interceptor”执行令牌注入,而“Authenticator”的唯一作用是检测令牌过期或撤销。 (2认同)
  • 新的官方网址是 https://square.github.io/okhttp/recipes/#handling-authentication-kt-java (2认同)

the*_*ang 63

如果您正在使用Retrofit > = 1.9.0那么您可以使用OkHttp的Interceptor,它是在OkHttp 2.2.0.您可能希望使用允许您使用的Application Interceptorretry 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;
    }

}
Run Code Online (Sandbox Code Playgroud)

定义之后Interceptor,创建一个OkHttpClient并将拦截器添加为Application Interceptor.

    OkHttpClient okHttpClient = new OkHttpClient();
    okHttpClient.interceptors().add(new CustomInterceptor());
Run Code Online (Sandbox Code Playgroud)

最后,OkHttpClient在创建你的时候使用它RestAdapter.

    RestService restService = new RestAdapter().Builder
            ...
            .setClient(new OkClient(okHttpClient))
            .create(RestService.class);
Run Code Online (Sandbox Code Playgroud)

警告:正如Jesse Wilson(来自Square)在这里提到的,这是一种危险的力量.

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

  • 当Android不允许主线程上的网络调用时,你如何在Android中实现同步调用?我遇到了从异步调用返回响应的问题. (2认同)
  • 除非我必须确保关闭之前的响应,否则我会泄漏以前的连接...最终请求newRequest = request.newBuilder().... build(); response.body()close()方法.return chain.proceed(newRequest); (2认同)

Dav*_*son 21

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

如果你有一个TokenService你需要的改造,Authenticator但你只想设置一个,OkHttpClient你可以使用一个TokenServiceHolder作为依赖TokenAuthenticator.您必须在应用程序(单例)级别维护对它的引用.如果您使用Dagger 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;
    }
Run Code Online (Sandbox Code Playgroud)

TokenServiceHolder.java:

public class TokenServiceHolder {

    TokenService tokenService = null;

    @Nullable
    public TokenService get() {
        return tokenService;
    }

    public void set(TokenService tokenService) {
        this.tokenService = tokenService;
    }
}
Run Code Online (Sandbox Code Playgroud)

客户端设置:

//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);
Run Code Online (Sandbox Code Playgroud)

如果您正在使用Dagger 2或类似的依赖注入框架,则在此问题的答案中有一些示例


Pha*_*inh 10

使用TokenAuthenticatorlike @theblang answer 是 handle 的正确方法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()
    }
}
Run Code Online (Sandbox Code Playgroud)

为了防止像@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>
}
Run Code Online (Sandbox Code Playgroud)

interface PotoAuthApi { // Authentication API
    @GET("api/images")
    fun getImage(): Single<GetImageResponse>
}
Run Code Online (Sandbox Code Playgroud)

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

AccessToken 班级

data class AccessToken(
        @Expose
        var token: String,

        @Expose
        var refreshToken: String)
Run Code Online (Sandbox Code Playgroud)

我的拦截器

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())
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,添加InterceptorAuthenticatorOKHttpClient创建服务时PotoAuthApi

演示

https://github.com/PhanVanLinh/AndroidMVPKotlin

笔记

身份验证器流程
  • 示例 APIgetImage()返回 401 错误代码
  • authenticate里面的方法TokenAuthenticator被触发
  • 同步noneAuthAPI.refreshToken(...)调用
  • noneAuthAPI.refreshToken(...)响应- >新的令牌会增加头
  • getImage()将使用新标头自动调用HttpLogging 不会记录此调用)(intercept内部AuthInterceptor 不会调用)
  • 如果getImage()仍然失败并出现错误 401,则authenticate内部方法TokenAuthenticator再次触发,然后将多次抛出有关调用方法的错误(java.net.ProtocolException: Too many follow-up requests)。您可以通过count response来防止它。例如,如果您return nullauthenticate3 次重试后,getImage()完成return response 401

  • 如果getImage()响应成功 => 我们将正常产生结果(就像您调用getImage()时没有错误一样)

希望有帮助