首页 文章

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

提问于
浏览
121

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

8 回答

  • 0

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

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

    当响应是 401 Not Authorised retrying last failed request 时,OkHttp将 automatically ask 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);
    
  • 1

    如果您正在使用Retrofit> = 1.9.0 ,则可以使用OkHttp's中引入的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());
    

    最后,在创建 RestAdapter 时使用此 OkHttpClient .

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

    Warning: 由于 Jesse Wilson (来自Square)提到here,这是一种危险的权力 .

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

  • 0

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

    如果你有 Authenticator 中需要的Retrofit TokenService ,但你只想设置一个 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;
        }
    

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

    如果你正在使用Dagger 2或类似的依赖注入框架,那么在this question的答案中有一些例子 .

  • 14

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

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

    我遇到了同样的问题,但我想只创建一个OkHttpClient因为我认为我不需要另一个只用于TokenAuthenticator本身,我使用的是Dagger2,所以我最终在TokenAuthenticator中提供服务类为 Lazy injected ,你可以在dagger 2 here中阅读更多关于懒惰注射的信息,但它基本上就是说要让Dagger去 NOT 并立即创建TokenAuthenticator所需的服务 .

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

  • 0

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

  • 60

    经过长期研究,我定制了Apache客户端来处理刷新AccessToken For Retrofit,其中您将访问令牌作为参数发送 .

    使用cookie Persistent Client启动适配器

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

    对于想要在刷新令牌时解决并发/并行调用的任何人 . 这是一个解决方法

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

    像_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()
        }
    }
    

    为防止 dependency cycle 喜欢@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)
    

    My Interceptor

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

    最后,添加 Interceptor 创建服务时 OKHttpClientOKHttpClient PotoAuthApi

    演示

    https://github.com/PhanVanLinh/AndroidMVPKotlin

    注意

    验证器流程

    • 示例API getImage() 返回401错误代码
      TokenAuthenticator 里面的

    • authenticate 方法将 fired

    • 同步 noneAuthAPI.refreshToken(...) 已调用

    • noneAuthAPI.refreshToken(...) 响应后 - >新标记将添加到 Headers

    • getImage()AUTO called 带新 Headers ( HttpLogging WILL NOT log 此次通话)( interceptAuthInterceptor WILL NOT CALLED

    • 如果 getImage() 仍然失败,错误401, TokenAuthenticator 中的 authenticate 方法将 fired AGAIN and AGAIN 那么它将多次抛出有关调用方法的错误( java.net.ProtocolException: Too many follow-up requests ) . 您可以通过count response来阻止它 . 例如,如果在 authenticatereturn null 重试3次后, getImage()finishreturn response 401

    • 如果 getImage() 响应成功=>我们将正常结果(就像你没有错误地调用 getImage()

    希望它有所帮助

相关问题