我们在我们的 Android 应用程序中使用 Retrofit 来与 OAuth2 安全服务器进行通信。一切正常,我们使用 RequestInterceptor 在每次调用中包含访问令牌。但是有时访问令牌将过期,并且需要刷新令牌。当令牌过期时,下一次调用将返回未经授权的 HTTP 代码,因此很容易监控。我们可以通过以下方式修改每个 Retrofit 调用:在失败回调中,检查错误代码,如果等于 Unauthorized,则刷新 OAuth 令牌,然后重复 Retrofit 调用。但是,为此,应该修改所有调用,这不是一个易于维护且良好的解决方案。有没有办法在不修改所有改造调用的情况下做到这一点?
10 回答
请不要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;
}
以与您相同的方式将 an 附加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);
如果你使用Retrofit >=1.9.0
那么你可以使用OkHttp 的新拦截器,它是在OkHttp 2.2.0
. 你会想要使用一个应用程序拦截器,它允许你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) {
// close previous response
response.close()
// 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);
警告:正如(来自 Square)在这里Jesse Wilson
提到的,这是一种危险的力量。
话虽如此,我绝对认为这是现在处理此类事情的最佳方式。如果您有任何问题,请随时在评论中提问。
TokenAuthenticator 依赖于一个服务类。服务类依赖于 OkHttpClient 实例。要创建 OkHttpClient,我需要 TokenAuthenticator。我怎样才能打破这个循环?两个不同的 OkHttpClients?他们将有不同的连接池..
TokenService
如果你有一个你需要的改造,Authenticator
但你只想设置一个OkHttpClient
,你可以使用 aTokenServiceHolder
作为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 或类似的依赖注入框架,则此问题的答案中有一些示例
使用TokenAuthenticator
like @theblang 答案是处理的正确方法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())
}
}
最后,在创建服务PotoAuthApi时Interceptor
添加和Authenticator
OKHttpClient
演示
https://github.com/PhanVanLinh/AndroidMVPKotlin
笔记
身份验证器流程- 示例 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()
时没有错误一样)
希望有帮助
我知道这是一个旧线程,但以防万一有人偶然发现它。
TokenAuthenticator 依赖于一个服务类。服务类依赖于 OkHttpClient 实例。要创建 OkHttpClient,我需要 TokenAuthenticator。我怎样才能打破这个循环?两个不同的 OkHttpClients?他们将有不同的连接池..
我遇到了同样的问题,但我只想创建一个 OkHttpClient 因为我认为我不需要另一个只用于 TokenAuthenticator 本身,我使用的是 Dagger2,所以我最终提供了服务类作为Lazy注入TokenAuthenticator,您可以在此处阅读更多关于 Dagger 2 中的延迟注入的信息,但这基本上就像是对 Dagger 说不要立即创建 TokenAuthenticator 所需的服务。
您可以参考这个 SO 线程的示例代码:如何在仍然使用 Dagger2 的同时解决循环依赖?
正如Brais Gabin在评论中所说,我遇到的问题TokenAuthenticator
取决于服务等级。服务类取决于一个OkHttpClient
实例并创建一个OkHttpClient
我需要的TokenAuthenticator
.
那么我是如何打破这个循环的呢?
我创建了一个新okHttpClient
对象,一个新Retrofit
对象,并使用该对象进行调用以使用refreshToken
(检查 getUpdatedToken() 函数)获取新令牌
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)
}
}
刷新令牌API
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)
因为这是我第一次实现这个我不知道这是否是最好的方式,但它是我项目中的工作方式。
使用一个拦截器(注入令牌)和一个验证器(刷新操作)来完成这项工作,但是:
我也遇到了双重调用问题:第一次调用总是返回 401:在第一次调用(拦截器)时没有注入令牌,并且调用了身份验证器:发出了两个请求。
修复只是为了重新影响拦截器中构建的请求:
前:
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 只使用身份验证器而不使用拦截器
您可以尝试为所有加载程序创建一个基类,您可以在其中捕获特定异常,然后根据需要执行操作。让所有不同的加载器从基类扩展,以传播行为。
经过长时间的研究,我定制了 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;
}
}
对于任何想要在刷新令牌时解决并发/并行调用的人。这是一个解决方法
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
}
}