android - how - oauth2 como funciona
Actualizar el token de OAuth con Retrofit sin modificar todas las llamadas (6)
TokenAuthenticator depende de una clase de servicio. La clase de servicio depende de una instancia OkHttpClient. Para crear un OkHttpClient necesito el TokenAuthenticator. ¿Cómo puedo romper este ciclo? ¿Dos OkHttpClients diferentes? Ellos van a tener diferentes grupos de conexiones ..
Si tiene, por ejemplo, un Retrofit TokenService
que necesita dentro de su Authenticator
pero solo desea configurar un OkHttpClient
, puede usar un TokenServiceHolder
como una dependencia para TokenAuthenticator
. Debería mantener una referencia en el nivel de aplicación (singleton). Esto es fácil si está usando Dagger 2; de lo contrario, simplemente cree un campo de clase dentro de su Aplicación.
En 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;
}
En TokenServiceHolder.java
:
public class TokenServiceHolder {
TokenService tokenService = null;
@Nullable
public TokenService get() {
return tokenService;
}
public void set(TokenService tokenService) {
this.tokenService = tokenService;
}
}
Configuración del cliente:
//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 está utilizando Dagger 2 o un marco de inyección de dependencia similar, hay algunos ejemplos en las respuestas a esta pregunta
Estamos utilizando Retrofit en nuestra aplicación de Android para comunicarnos con un servidor seguro de OAuth2. Todo funciona muy bien, usamos RequestInterceptor para incluir el token de acceso con cada llamada. Sin embargo, habrá momentos en los que el token de acceso caducará y el token deberá actualizarse. Cuando el token caduque, la próxima llamada volverá con un código HTTP no autorizado, por lo que es fácil de controlar. Podríamos modificar cada llamada de Retrofit de la siguiente manera: En la devolución de llamada fallida, verifique el código de error, si es No autorizado, actualice el token de OAuth y luego repita la llamada de Retrofit. Sin embargo, para esto, todas las llamadas deberían modificarse, lo cual no es una solución fácil de mantener y buena. ¿Hay alguna forma de hacerlo sin modificar todas las llamadas de Retrofit?
Después de una larga investigación, personalicé el cliente Apache para manejar Refreshing AccessToken For Retrofit En el que envía token de acceso como parámetro.
Inicie su adaptador con Cookie Persistent Client
restAdapter = new RestAdapter.Builder()
.setEndpoint(SERVER_END_POINT)
.setClient(new CookiePersistingClient())
.setLogLevel(RestAdapter.LogLevel.FULL).build();
Cookie Persistent Client que mantiene cookies para todas las solicitudes y verificaciones con cada respuesta de solicitud, si es un acceso no autorizado ERROR_CODE = 401, actualiza el token de acceso y recupera la solicitud, de lo contrario solo procesa la solicitud.
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;
}
}
Para cualquier persona que desee resolver llamadas concurrentes / paralelas al actualizar el token. Aquí hay una solución
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
}
}
Puede intentar crear una clase base para todos sus cargadores en la que pueda detectar una excepción en particular y actuar según lo necesite. Haga que todos los cargadores diferentes se extiendan desde la clase base para distribuir el comportamiento.
Si está utilizando Retrofit > = 1.9.0
, puede utilizar OkHttp''s nuevo github.com/square/okhttp/wiki/Interceptors de OkHttp''s , que se introdujo en OkHttp 2.2.0
. Desearía utilizar un Interceptor de aplicación , que le permite retry and make multiple calls
a retry and make multiple calls
.
Su interceptor podría parecerse a este pseudocódigo:
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;
}
}
Después de definir su Interceptor
, cree un OkHttpClient
y agregue el interceptor como Interceptor de Aplicación .
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.interceptors().add(new CustomInterceptor());
Y finalmente, use este OkHttpClient
al crear su RestAdapter
.
RestService restService = new RestAdapter().Builder
...
.setClient(new OkClient(okHttpClient))
.create(RestService.class);
Advertencia: como menciona Jesse Wilson
(de Square) here , esta es una cantidad de poder peligrosa.
Habiendo dicho eso, definitivamente creo que esta es la mejor manera de manejar algo como esto ahora. Si tiene alguna pregunta, no dude en preguntar en un comentario.
Por favor, no use Interceptors
para tratar con la autenticación.
Actualmente, el mejor enfoque para manejar la autenticación es usar la nueva API Authenticator
, diseñada específicamente para este propósito .
OkHttp le pedirá automáticamente al Authenticator
credenciales cuando la respuesta sea 401 Not Authorised
reintentando la última solicitud fallida con ellas.
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;
}
Adjunte un Authenticator
a un OkHttpClient
la misma manera que lo hace con los Interceptors
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setAuthenticator(authAuthenticator);
Utilice este cliente al crear su RestAdapter
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(ENDPOINT)
.setClient(new OkClient(okHttpClient))
.build();
return restAdapter.create(API.class);