Android 使用 Retrofit 刷新 OAuth 令牌而不修改所有调用
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/22450036/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Refreshing OAuth token using Retrofit without modifying all calls
提问by Daniel Zolnai
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?
我们在 Android 应用程序中使用 Retrofit 与 OAuth2 安全服务器进行通信。一切正常,我们使用 RequestInterceptor 在每次调用中包含访问令牌。但是,有时访问令牌会过期,并且需要刷新令牌。当令牌过期时,下一次调用将返回一个未经授权的 HTTP 代码,以便于监控。我们可以通过以下方式修改每个 Retrofit 调用:在失败回调中,检查错误代码,如果它等于 Unauthorized,则刷新 OAuth 令牌,然后重复 Retrofit 调用。但是,为此,应该修改所有调用,这不是一个易于维护的好解决方案。有没有办法在不修改所有 Retrofit 调用的情况下做到这一点?
回答by lgvalle
Please do not use Interceptors
to deal with authentication.
请不要Interceptors
用于处理身份验证。
Currently, the best approach to handle authentication is to use the new Authenticator
API, designed specifically for this purpose.
目前,处理身份验证的最佳方法是使用Authenticator
专门为此目的设计的新API 。
OkHttp will automatically askthe Authenticator
for credentials when a response is 401 Not Authorised
retrying last failed requestwith them.
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;
}
Attach an Authenticator
to an OkHttpClient
the same way you do with Interceptors
Authenticator
以OkHttpClient
与您相同的方式将 an 附加到Interceptors
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setAuthenticator(authAuthenticator);
Use this client when creating your Retrofit
RestAdapter
创建您的客户端时使用此客户端 Retrofit
RestAdapter
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(ENDPOINT)
.setClient(new OkClient(okHttpClient))
.build();
return restAdapter.create(API.class);
回答by theblang
If you are using Retrofit>= 1.9.0
then you could make use of OkHttp'snew Interceptor, which was introduced in OkHttp 2.2.0
. You would want to use an Application Interceptor, which permits you to retry and make multiple calls
.
如果您正在使用Retrofit>=1.9.0
那么您可以使用OkHttp 的新Interceptor,它是在OkHttp 2.2.0
. 您可能希望使用应用程序拦截器,它允许您这样做retry and make multiple calls
。
Your Interceptor could look something like this pseudocode:
您的拦截器可能类似于以下伪代码:
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;
}
}
After you define your Interceptor
, create an OkHttpClient
and add the interceptor as an Application Interceptor.
在您定义您的Interceptor
、创建OkHttpClient
并添加拦截器作为应用程序拦截器之后。
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.interceptors().add(new CustomInterceptor());
And finally, use this OkHttpClient
when creating your RestAdapter
.
最后,OkHttpClient
在创建RestAdapter
.
RestService restService = new RestAdapter().Builder
...
.setClient(new OkClient(okHttpClient))
.create(RestService.class);
Warning:As Jesse Wilson
(from Square) mentions here, this is a dangerous amount of power.
警告:正如Jesse Wilson
(来自 Square)在这里提到的那样,这是一种危险的力量。
With that being said, I definitely think this is the best way to handle something like this now. If you have any questions please don't hesitate to ask in a comment.
话虽如此,我绝对认为这是现在处理此类事情的最佳方式。如果您有任何问题,请随时在评论中提问。
回答by David Rawson
TokenAuthenticator depends an a service class. The service class depends on an OkHttpClient instance. To create an OkHttpClient I need the TokenAuthenticator. How can I break this cycle? Two different OkHttpClients? They are going to have different connection pools..
TokenAuthenticator 依赖于一个服务类。服务类依赖于 OkHttpClient 实例。要创建 OkHttpClient,我需要 TokenAuthenticator。我怎样才能打破这个循环?两个不同的 OkHttpClients?他们将有不同的连接池..
If you have, say, a Retrofit TokenService
that you need inside your Authenticator
but you would only like to set up one OkHttpClient
you can use a TokenServiceHolder
as a dependency for TokenAuthenticator
. You would have to maintain a reference to it at the application (singleton) level. This is easy if you are using Dagger 2, otherwise just create class field inside your Application.
比如说,如果你有一个TokenService
你需要的 Retrofit ,Authenticator
但你只想设置一个OkHttpClient
你可以使用 aTokenServiceHolder
作为TokenAuthenticator
. 您必须在应用程序(单例)级别维护对它的引用。如果您使用 Dagger 2,这很容易,否则只需在您的应用程序中创建类字段。
In TokenAuthenticator.java
在 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;
}
In TokenServiceHolder.java
:
在TokenServiceHolder.java
:
public class TokenServiceHolder {
TokenService tokenService = null;
@Nullable
public TokenService get() {
return tokenService;
}
public void set(TokenService tokenService) {
this.tokenService = tokenService;
}
}
Client setup:
客户端设置:
//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);
If you are using Dagger 2 or a similar dependency injection framework there are some examples in the answers to this question
如果您使用的是 Dagger 2 或类似的依赖注入框架,则此问题的答案中有一些示例
回答by Phan Van Linh
Using TokenAuthenticator
like @theblang answer is a correct way for handle refresh_token
.
使用TokenAuthenticator
like @theblang answer 是 handle 的正确方法refresh_token
。
Here is my implement (I have using Kotlin, Dagger, RX but you may use this idea for implement to your case)TokenAuthenticator
这是我的实现(我使用了 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()
}
}
For prevent dependency cyclelike @Brais Gabin comment, I create 2interface like
为了防止像@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
班级
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
class
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())
}
}
Finally, add Interceptor
and Authenticator
to your OKHttpClient
when create service PotoAuthApi
最后,添加Interceptor
和Authenticator
您OKHttpClient
创建服务时PotoAuthApi
Demo
演示
https://github.com/PhanVanLinh/AndroidMVPKotlin
https://github.com/PhanVanLinh/AndroidMVPKotlin
Note
笔记
身份验证器流程- Example API
getImage()
return 401 error code authenticate
method insideTokenAuthenticator
will fired- Synchronize
noneAuthAPI.refreshToken(...)
called - After
noneAuthAPI.refreshToken(...)
response -> new token will add to header getImage()
will AUTO calledwith new header (HttpLogging
WILL NOT logthis call) (intercept
insideAuthInterceptor
WILL NOT CALLED)If
getImage()
still failed with error 401,authenticate
method insideTokenAuthenticator
will fired AGAIN and AGAINthen 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 youreturn null
inauthenticate
after 3 times retry,getImage()
will finishandreturn response 401
If
getImage()
response success => we will result the result normally (like you callgetImage()
with no error)
- 示例 API
getImage()
返回 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 null
在authenticate
3 次重试后,getImage()
将完成并return response 401
如果
getImage()
响应成功 => 我们将正常产生结果(就像您调用getImage()
时没有错误一样)
Hope it help
希望有帮助
回答by Boda
I know this an old thread, but just in case someone stumbled in it.
我知道这是一个旧线程,但以防万一有人偶然发现它。
TokenAuthenticator depends an a service class. The service class depends on an OkHttpClient instance. To create an OkHttpClient I need the TokenAuthenticator. How can I break this cycle? Two different OkHttpClients? They are going to have different connection pools..
TokenAuthenticator 依赖于一个服务类。服务类依赖于 OkHttpClient 实例。要创建 OkHttpClient,我需要 TokenAuthenticator。我怎样才能打破这个循环?两个不同的 OkHttpClients?他们将有不同的连接池..
I was facing the same problem, but I wanted to create only one OkHttpClient becuase I don't think that I need another one for just the TokenAuthenticator itself, I was using Dagger2, so I ended up providing the service class as Lazy injectedin the TokenAuthenticator, you can read more about Lazy injection in dagger 2 here, but it's like basically saying to Dagger to NOTgo and create the service needed by the TokenAuthenticator right away.
我遇到了同样的问题,但我只想创建一个 OkHttpClient,因为我认为我不需要另一个只用于 TokenAuthenticator 本身,我使用的是 Dagger2,所以我最终提供了服务类,因为Lazy 注入了TokenAuthenticator,您可以在此处阅读有关Dagger 2 中的延迟注入的更多信息,但这基本上是在告诉 Dagger不要立即创建 TokenAuthenticator 所需的服务。
You can refer to this SO thread for sample code: How to resolve a circular dependency while still using Dagger2?
您可以参考此 SO 线程获取示例代码:如何在仍在使用 Dagger2 的同时解决循环依赖?
回答by Sigrun
Using one Interceptor (inject the token) and one Authenticator (refresh operations) do the job but:
使用一个拦截器(注入令牌)和一个身份验证器(刷新操作)可以完成这项工作,但是:
I had a double call problem too: the first call always returned a 401: the token wasn't inject at the first call (interceptor) and the authenticator was called: two requests were made.
我也有一个双重调用问题:第一次调用总是返回 401:令牌没有在第一次调用(拦截器)时注入,并且调用了身份验证器:发出了两个请求。
The fix was just to reaffect the request to the build in the Interceptor:
修复只是重新影响对拦截器中构建的请求:
BEFORE:
前:
private Interceptor getInterceptor() {
return (chain) -> {
Request request = chain.request();
//...
request.newBuilder()
.header(AUTHORIZATION, token))
.build();
return chain.proceed(request);
};
}
AFTER:
后:
private Interceptor getInterceptor() {
return (chain) -> {
Request request = chain.request();
//...
request = request.newBuilder()
.header(AUTHORIZATION, token))
.build();
return chain.proceed(request);
};
}
IN ONE BLOCK:
一个街区:
private Interceptor getInterceptor() {
return (chain) -> {
Request request = chain.request().newBuilder()
.header(AUTHORIZATION, token))
.build();
return chain.proceed(request);
};
}
Hope it helps.
希望能帮助到你。
Edit:I didn't find a way to avoid the first call to always returning 401 using only the authenticator and no interceptor
编辑:我没有找到一种方法来避免第一次调用总是只使用身份验证器而不使用拦截器返回 401
回答by Suneel Prakash
After Long research, I customized Apache client to handle Refreshing AccessToken For Retrofit In which you send access token as parameter.
经过长时间的研究,我定制了 Apache 客户端来处理 Refreshing AccessToken For Retrofit 在其中发送访问令牌作为参数。
Initiate your Adapter with cookie Persistent Client
使用 cookie Persistent Client 启动您的适配器
restAdapter = new RestAdapter.Builder()
.setEndpoint(SERVER_END_POINT)
.setClient(new CookiePersistingClient())
.setLogLevel(RestAdapter.LogLevel.FULL).build();
Cookie Persistent client which maintains cookies for all requests and checks with each request response, if it is unauthorized access ERROR_CODE = 401, refresh access token and recall the request, else just processes request.
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;
}
}
回答by k3v1n4ud3
You can try creating a base class for all your loaders in which you would be able to catch a particular exception and then act as you need. Make all your different loaders extend from the base class in order to spread the behaviour.
您可以尝试为所有加载器创建一个基类,您可以在其中捕获特定异常,然后根据需要执行操作。使所有不同的加载器从基类扩展以传播行为。
回答by Anders Cheow
To anyone who wanted to solve concurrent/parallel calls when refreshing token. Here's a workaround
对于任何想在刷新令牌时解决并发/并行调用的人。这是一个解决方法
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
}
}