Nous utilisons Retrofit dans notre Android application) pour communiquer avec un serveur sécurisé OAuth2. Tout fonctionne parfaitement, nous utilisons le RequestInterceptor pour inclure le jeton d'accès à chaque appel. Cependant, il y aura des moments où le jeton d'accès expirera et le jeton devra être actualisé. Lorsque le jeton expirera, le prochain appel sera renvoyé avec un code HTTP non autorisé, ce qui facilitera la surveillance. Nous pourrions modifier chaque appel de modification de la manière suivante: Dans le rappel d'échec , recherchez le code d'erreur; s'il est égal à Non autorisé, actualisez le jeton OAuth, puis répétez l'appel de modification. Toutefois, pour cela, tous les appels doivent être modifiés, ce qui n'est pas facilement gérable, et bonne solution: existe-t-il un moyen de le faire sans modifier tous les appels de modification?
Merci de ne pas utiliser Interceptors
pour gérer l'authentification.
Actuellement, la meilleure approche pour gérer l'authentification consiste à utiliser la nouvelle API Authenticator
, conçue spécifiquement pour cette fin .
OkHttp va demander automatiquement le Authenticator
pour obtenir les informations d'identification lorsqu'une réponse est 401 Not Authorised
réessaie avec la dernière demande échouée .
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;
}
Attachez un Authenticator
à un OkHttpClient
comme vous le feriez avec Interceptors
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setAuthenticator(authAuthenticator);
Utilisez ce client lors de la création de votre Retrofit
RestAdapter
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(ENDPOINT)
.setClient(new OkClient(okHttpClient))
.build();
return restAdapter.create(API.class);
Si vous utilisez Retrofit > = 1.9.0
alors vous pouvez utiliser OkHttp's new Interceptor , qui a été introduit dans OkHttp 2.2.0
. Vous voudriez utiliser un Application Interceptor , qui vous permet de retry and make multiple calls
.
Votre intercepteur pourrait ressembler à ce 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;
}
}
Après avoir défini votre Interceptor
, créez un OkHttpClient
et ajoutez l'intercepteur en tant que Application Interceptor .
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.interceptors().add(new CustomInterceptor());
Et enfin, utilisez ceci OkHttpClient
lors de la création de votre RestAdapter
.
RestService restService = new RestAdapter().Builder
...
.setClient(new OkClient(okHttpClient))
.create(RestService.class);
Attention: As Jesse Wilson
(de Square) mentionne ici , il s’agit d’une quantité de pouvoir dangereuse.
Cela étant dit, je pense vraiment que c’est la meilleure façon de gérer une situation de ce type maintenant. Si vous avez des questions s'il vous plaît n'hésitez pas à demander dans un commentaire.
TokenAuthenticator dépend d'une classe de service. La classe de service dépend d'une instance OkHttpClient. Pour créer un OkHttpClient, j'ai besoin de TokenAuthenticator. Comment puis-je briser ce cycle? Deux OkHttpClients différents? Ils vont avoir différents pools de connexion.
Si vous possédez, par exemple, un Retrofit TokenService
dont vous avez besoin à l'intérieur de votre Authenticator
mais que vous ne souhaitez configurer qu'un seul OkHttpClient
, vous pouvez utiliser un correcteur TokenServiceHolder
en tant que dépendance de TokenAuthenticator
. Vous devez conserver une référence au niveau de l'application (singleton). Cela est facile si vous utilisez Dagger 2, sinon créez simplement un champ de classe dans votre application.
Dans 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;
}
Dans TokenServiceHolder.Java
:
public class TokenServiceHolder {
TokenService tokenService = null;
@Nullable
public TokenService get() {
return tokenService;
}
public void set(TokenService tokenService) {
this.tokenService = tokenService;
}
}
Configuration du client:
//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);
Si vous utilisez Dagger 2 ou un framework d'injection de dépendance similaire, vous trouverez des exemples dans les réponses à cette question
Utiliser TokenAuthenticator
comme @theblang answer est un moyen correct pour gérer refresh_token
.
Voici mon outil (j'ai déjà utilisé Kotlin, Dagger, RX mais vous pouvez utiliser cette idée pour mettre en œuvre votre cas)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()
}
}
Pour empêcher le cycle de dépendance comme @Brais Gabin commenter, je crée une interface 2 comme
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>
}
et
interface PotoAuthApi { // Authentication API
@GET("api/images")
fun getImage(): Single<GetImageResponse>
}
AccessTokenWrapper
classe
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
classe
data class AccessToken(
@Expose
var token: String,
@Expose
var refreshToken: String)
Mon intercepteur
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())
}
}
Enfin, ajoutez Interceptor
et Authenticator
à votre OKHttpClient
lors de la création du service PotoAuthApi
https://github.com/PhanVanLinh/AndroidMVPKotlin
getImage()
retourne un code d'erreur 401authenticate
la méthode à l'intérieur de TokenAuthenticator
sera déclenchée noneAuthAPI.refreshToken(...)
appeléenoneAuthAPI.refreshToken(...)
réponse -> le nouveau jeton s'ajoutera à l'en-têtegetImage()
sera AUTO appelé avec un nouvel en-tête (HttpLogging
NE SERA PAS connecté cet appel) (intercept
à l'intérieur AuthInterceptor
NE SERA PAS APPELÉ )Si getImage()
échouait toujours avec l'erreur 401, la méthode authenticate
à l'intérieur de TokenAuthenticator
se déclencherait à nouveau et à nouveau alors il va jeter erreur sur la méthode d'appel plusieurs fois (Java.net.ProtocolException: Too many follow-up requests
). Vous pouvez l’empêcher par nombre de réponses . Exemple, si vous return null
Dans authenticate
après trois tentatives, getImage()
finira et return response 401
Si getImage()
réponse réussie => nous obtiendrons le résultat normalement (comme vous appelez getImage()
sans erreur)
J'espère que ça aide
Je sais que c'est un vieux fil, mais juste au cas où quelqu'un trébucherait dedans.
TokenAuthenticator dépend d'une classe de service. La classe de service dépend d'une instance OkHttpClient. Pour créer un OkHttpClient, j'ai besoin de TokenAuthenticator. Comment puis-je briser ce cycle? Deux OkHttpClients différents? Ils vont avoir différents pools de connexion.
Je faisais face au même problème, mais je voulais créer un seul OkHttpClient car je ne pensais pas en avoir besoin d’un autre pour TokenAuthenticator lui-même, j’utilisais Dagger2. J’ai donc fourni la classe de service sous la forme Lazy injecté dans TokenAuthenticator, vous pouvez en savoir plus sur Lazy injection in dagger 2 ici , mais c'est comme dire fondamentalement à Dagger de [~ # ~] pas [~ # ~] va immédiatement créer le service requis par TokenAuthenticator.
Vous pouvez vous référer à ce SO thread pour exemple de code: Comment résoudre une dépendance circulaire tout en utilisant toujours Dagger2?
Après de longues recherches, j'ai personnalisé le client Apache pour qu'il gère l'actualisation de AccessToken for Retrofit dans lequel vous envoyez un jeton d'accès en tant que paramètre.
Initiez votre adaptateur avec le client persistant cookie
restAdapter = new RestAdapter.Builder()
.setEndpoint(SERVER_END_POINT)
.setClient(new CookiePersistingClient())
.setLogLevel(RestAdapter.LogLevel.FULL).build();
Cookie Client permanent qui conserve les cookies pour toutes les demandes et vérifie chaque réponse à la demande. S'il s'agit d'un accès non autorisé, ERROR_CODE = 401, actualise le jeton d'accès et rappelle la demande, sinon il ne traite que la demande.
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;
}
}
Vous pouvez essayer de créer une classe de base pour tous vos chargeurs dans laquelle vous pourrez intercepter une exception particulière, puis agir selon vos besoins. Faites en sorte que tous vos chargeurs soient étendus à partir de la classe de base afin de répandre le comportement.
Utiliser un intercepteur (injecter le jeton) et un authentificateur (opérations de rafraîchissement) remplit son rôle, mais:
J'ai également eu un problème de double appel: le premier appel a toujours renvoyé un 401: le jeton n'a pas été injecté au premier appel (intercepteur) et l'authentificateur a été appelé: deux demandes ont été effectuées.
Le correctif consistait simplement à réaffecter la demande à la construction de l'intercepteur:
AVANT:
private Interceptor getInterceptor() {
return (chain) -> {
Request request = chain.request();
//...
request.newBuilder()
.header(AUTHORIZATION, token))
.build();
return chain.proceed(request);
};
}
APRÈS:
private Interceptor getInterceptor() {
return (chain) -> {
Request request = chain.request();
//...
request = request.newBuilder()
.header(AUTHORIZATION, token))
.build();
return chain.proceed(request);
};
}
EN UN BLOC:
private Interceptor getInterceptor() {
return (chain) -> {
Request request = chain.request().newBuilder()
.header(AUTHORIZATION, token))
.build();
return chain.proceed(request);
};
}
J'espère que ça aide.
Edit: Je n'ai pas trouvé le moyen d'éviter le premier appel pour toujours renvoyer le 401 en utilisant uniquement l'authentificateur et aucun intercepteur