使用翻新功能刷新OAuth令牌,而无需修改所有调用


157

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


1
这看起来与我的其他问题有关。我很快会再次调查,但是一种可能的方法是包装OkHttpClient。像这样的东西:github.com/pakerfeldt/signpost-retrofit 另外,由于我将RoboSpice与Retrofit一起使用,因此创建基本Request类可能也是另一种可能的方法。不过,也许您必须弄清楚如何在没有上下文的情况下实现流程,例如使用Otto / EventBus。
哈桑·易卜拉欣2014年

1
好吧,您可以将其分叉,并删除不需要的箱子。我可能会在今天进行调查,如果我能解决我们的问题,请在此处发布。
Daniel Zolnai 2014年

2
原来,该库没有处理刷新令牌,但是给了我一个主意。我对一些未经测试的代码做了一个小观点,但从理论上讲,我认为它应该起作用:gist.github.com/ZolnaiDani/9710849
Daniel Zolnai 2014年

3
@neworld我可以想到的一种解决方案:使changeTokenInRequest(...)同步,并在第一行检查何时最后一次刷新令牌。如果仅几秒钟(毫秒)之前,请勿刷新令牌。您还可以将此时间范围设置为1小时左右,以在令牌之外的其他问题过时时停止不断请求新令牌。
Daniel Zolnai 2014年

2
Retrofit 1.9.0刚刚添加了对具有拦截器的OkHttp 2.2的支持。这应该使您的工作容易得多。有关更多信息,请参见:github.com/square/retrofit/blob/master/…github.com/square/okhttp/wiki/Interceptors您也必须为这些扩展OkHttp。
Daniel Zolnai 2015年

Answers:


213

请不要使用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;
    }

AuthenticatorOkHttpClient与您相同的方式附加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);

6
这是否意味着每个请求将始终失败1次,还是在执行请求时添加令牌?
Jdruwe

11
@Jdruwe看起来这段代码将失败1次,然后它将发出请求。但是,如果添加拦截器的唯一目的是始终添加访问令牌(无论访问令牌是否已过期),则仅在接收到401(仅在该令牌已过期时才会发生)时才调用此访问令牌。
narciero

54
TokenAuthenticator取决于一service类。该service等级取决于一个OkHttpClient实例。要创建一个OkHttpClient我需要TokenAuthenticator。我怎样才能打破这个周期?两个不同的OkHttpClients?他们将有不同的连接池...
Brais Gabin

6
需要刷新令牌的许多并行请求怎么样?同时将有多个刷新令牌请求。如何避免呢?
Igor Kostenko

10
好的,所以解决@Ihor问题的方法可能是同步Authenticator中的代码。它解决了我的问题。在Request authenticate(...)方法中:-执行任何初始化操作-启动同步块(synced(MyAuthenticator.class){...})-在该块中检索当前访问和刷新令牌-检查失败的请求是否正在使用最新的访问令牌(resp.request()。header(“ Authorization”))-如果不只是使用更新的访问令牌再次运行它-否则运行刷新令牌流-更新/持久更新访问和刷新令牌-完成同步块-重新运行
Dariusz Wiechecki

65

如果您使用的是Retrofit > =,1.9.0则可以使用OkHttp的Interceptor(已在中引入)OkHttp 2.2.0。您可能需要使用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并将拦截器添加为应用程序拦截器

    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(从广场)提到这里,这是权力的危险量。

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


2
当Android不允许在主线程上进行网络调用时,如何在Android中实现同步调用?我遇到了从异步调用返回响应的问题。
lgdroid57

1
@ lgdroid57您是正确的,因此在启动触发拦截器运行的原始请求时,您应该已经在另一个线程上。
theblang

3
效果很好,除了我必须确保关闭先前的响应,否则我将泄漏先前的连接... final Request newRequest = request.newBuilder().... build(); response.body()。close(); return chain.proceed(newRequest);
DallinDyer

谢谢!我遇到了一个问题,即原始请求的回调收到的消息是“关闭”而不是原始响应,这是由于主体在拦截器中被消耗了。我能够解决此问题以获得成功的响应,但是我无法解决此失败的响应。有什么建议?
lgdroid57

谢谢@mattblang,看起来不错。一个问题:即使在重试时,请求回调也能保证被调用吗?
卡·法乔利

23

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

举例来说,如果您有一个TokenService内部需要的翻新产品,Authenticator但只想设置一个翻新产品,则OkHttpClient可以将TokenServiceHolder用作依赖项TokenAuthenticator。您将必须在应用程序(单个)级别上维护对它的引用。如果使用Dagger 2,这很容易,否则只需在Application内创建类字段。

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或类似的依赖项注入框架,则此问题的答案中将包含一些示例


在哪里TokenService创建类?
Yogesh Suthar '17

@YogeshSuthar这是一项改造服务-请参阅相关问题
David Rawson

谢谢,您能提供refreshToken()from 的实施吗service.refreshToken().execute();?在任何地方都找不到它的实现。
Yogesh Suthar's

@Yogesh refreshToken方法来自您的API。无论您调用什么来刷新令牌(可能是使用用户名和密码的调用?)。也许是您提交令牌的请求,响应是一个新令牌
David Rawson

5

使用TokenAuthenticatorlike @theblang答案是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()
    }
}

为了防止像@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())
    }
}

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

演示版

https://github.com/PhanVanLinh/AndroidMVPKotlin

注意

认证者流程
  • 示例API getImage()返回401错误代码
  • authenticate里面的方法TokenAuthenticator被解雇
  • 同步noneAuthAPI.refreshToken(...)调用
  • noneAuthAPI.refreshToken(...)响应- >新的令牌会增加头
  • getImage()将使用新的标头自动调用HttpLogging 不会记录此调用)(intercept内部AuthInterceptor 不会调用)
  • 如果getImage()仍然由于错误401而失败,则authenticate内部方法TokenAuthenticator触发AGAIN和AGAIN,然后它将多次抛出有关调用方法的错误java.net.ProtocolException: Too many follow-up requests。您可以通过计数响应来阻止它。例如,如果你return nullauthenticate经过3次重试,getImage()完成return response 401

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

希望对你有帮助


该解决方案使用2个不同的OkHttpClients,如您的ServiceGenerator类中所示。
SpecialSnowflake

@SpecialSnowflake你是对的。如果您遵循我的解决方案,则需要创建2 OkHttp,因为我创建了2服务(oauth和none auth)。我认为不会造成任何问题。让我知道您的想法
Phan Van Linh

1

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

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

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

您可以参考此SO线程获取示例代码:如何在仍使用Dagger2的情况下解决循环依赖关系?


0

您可以尝试为所有装入程序创建基类,在其中可以捕获特定异常,然后根据需要进行操作。让您所有不同的装载机都从基类扩展以扩展行为。


改造不是那样的。它使用Java注释和接口来描述API调用
Daniel Zolnai 2014年

我知道改型的工作原理,但是您仍在“包装” AsynTask中的API调用,不是吗?
k3v1n4ud3 2014年

不,我将这些调用与Callback一起使用,因此它们异步运行。
Daniel Zolnai 2014年

然后,您可能可以创建一个基础回调类,并使所有回调对其进行扩展。
k3v1n4ud3 2014年

2
有什么解决办法吗?这正是我的情况。= /
Hugo Nogueira 2014年

0

经过长时间的研究,我定制了Apache客户端来处理Refreshing 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;
    }
}

您是否有理由使用ApacheClient而不是建议的解决方案?并不是说这不是一个好的解决方案,但是与使用Interceptor相比,它需要更多的编码。
Daniel Zolnai 2015年

它被定制为cookie持久客户端,可维护整个服务的会话。即使在请求拦截器中,您也可以在标头中添加访问令牌。但是,如果您想将其添加为参数怎么办?OKHTTPClient也有局限性。REF:stackoverflow.com/questions/24594823/...
Suneel普拉卡什

它在任何情况下都更通用。1. Cookie持久客户端2.接受HTTP和HTTPS请求3.在Params中更新访问令牌。
Suneel Prakash

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的第一次调用


-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
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.