android retrofit jwt
Android Retrofit2 Actualizar Oauth 2 Token (2)
Estoy usando las bibliotecas Retrofit
y OkHttp
. Así que tengo un Authenticator
que Authenticator
al usuario si obtiene 401 respuestas.
Mi build.gradle
es así:
compile ''com.squareup.retrofit2:retrofit:2.0.0-beta4''
compile ''com.squareup.retrofit2:converter-gson:2.0.0-beta4''
compile ''com.squareup.okhttp3:okhttp:3.1.2''
Y mi Authenticator
personalizado está aquí:
import java.io.IOException;
import okhttp3.Authenticator;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;
public class CustomAuthanticator implements Authenticator {
@Override
public Request authenticate(Route route, Response response) throws IOException {
//refresh access token via refreshtoken
Retrofit client = new Retrofit.Builder()
.baseUrl(baseurl)
.addConverterFactory(GsonConverterFactory.create())
.build();
APIService service = client.create(APIService.class);
Call<RefreshTokenResult> refreshTokenResult=service.refreshUserToken("application/json", "application/json", "refresh_token",client_id,client_secret,refresh_token);
//this is syncronous retrofit request
RefreshTokenResult refreshResult= refreshTokenResult.execute().body();
//check if response equals 400 , mean empty response
if(refreshResult!=null) {
//save new access and refresh token
// than create a new request and modify it accordingly using the new token
return response.request().newBuilder()
.header("Authorization", newaccesstoken)
.build();
} else {
//we got empty response and return null
//if we dont return null this method is trying to make so many request
//to get new access token
return null;
}
}}
Esta es mi clase de APIService
:
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.Query;
public interface APIService {
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST("token")
public Call<RefreshTokenResult> refreshUserToken(@Header("Accept") String accept,
@Header("Content-Type") String contentType, @Field("grant_type") String grantType,
@Field("client_id") String clientId, @Field("client_secret") String clientSecret,
@Field("refresh_token") String refreshToken);
}
Estoy usando un autantenador así:
CustomAuthanticator customAuthanticator=new CustomAuthanticator();
OkHttpClient okClient = new OkHttpClient.Builder()
.authenticator(customAuthanticator)
.build();
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd''T''HH:mm:ssZ")
.create();
Retrofit client = new Retrofit.Builder()
.baseUrl(getResources().getString(R.string.base_api_url))
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okClient)
.build();
//then make retrofit request
Así que mi pregunta es: a veces obtengo un nuevo token de acceso y continúo trabajando, realizando nuevas solicitudes. Pero a veces obtengo 400 respuestas, lo que significa una respuesta vacía. Así que mi antiguo token de actualización no es válido y no puedo obtener un nuevo token. Normalmente nuestro token de actualización caduca en 1 año. Entonces, ¿cómo puedo hacer esto. Por favor, ayúdame !
Descargo de responsabilidad : en realidad estoy usando
Dagger
+RxJava
+RxAndroid
+Retrofit
pero solo quería proporcionar una respuesta para demostrar la lógica a futuros visitantes. La única diferencia es usarSchedulers.trampoline()
al actualizar tu token para bloquear ese hilo. Si tiene más preguntas sobre estas bibliotecas, por favor comente a continuación, así que quizás pueda proporcionarle otra respuesta o ayuda.
también
Importante Lea esto : si está realizando solicitudes al mismo tiempo pero también está utilizando
dispatcher.setMaxRequests(1);
su token se actualizará varias veces dentro de la claseTokenInterceptor
. Por ejemplo, cuando su aplicación y su servicio realizan solicitudes al mismo tiempo. Para superar este problema, simplemente agregue una palabra clavesynchronized
a su método deintercept
dentro deTokenInterceptor
:public synchronized Response intercept(Chain chain)
@Edit 07.04.2017:
Actualicé esta respuesta porque era un poco antigua y mis circunstancias cambiaron, ahora tengo un servicio en segundo plano que también hace solicitudes.
En primer lugar, el proceso de actualización del token es un proceso crítico. En mi aplicación y en la mayoría de las aplicaciones, haga lo siguiente: si el token de actualización falla, cierre la sesión del usuario actual y adviértale al usuario que inicie sesión. (Tal vez pueda reintentar el proceso del token de actualización 2-3-4 veces, dependiendo de usted)
@ Aviso importante : realice solicitudes síncronas cuando actualice su token dentro de Authenticator
o Interceptor
ya que debe bloquear ese hilo hasta que su solicitud finalice, de lo contrario, sus solicitudes se ejecutarán dos veces con el token nuevo y el anterior.
De todas formas lo explicaré paso a paso:
Paso 1: consulte el patrón de singleton , crearemos una clase responsable de devolver nuestra instancia de actualización siempre que lo desee, desde cualquier lugar al que queramos acceder. Dado que es estático, si no hay una instancia disponible, solo crea la instancia una vez y cuando la llama, siempre devuelve esta instancia estática. Esta es también la definición básica del patrón de diseño de Singleton.
public class RetrofitClient {
private static Retrofit retrofit = null;
private RetrofitClient() {
// this default constructor is private and you can''t call it like :
// RetrofitClient client = new RetrofitClient();
// only way to get it : Retrofit client = RetrofitClient.getInstance();
}
public static Retrofit getInstance() {
if (retrofit == null) {
// my token authenticator, I will add this class at below
TokenAuthenticator tokenAuthenticator = new TokenAuthenticator();
// I am also using interceptor which controls token if expired
// lets look at this scenario : My token needs to refresh after 10 hours
// but I came to application after 50 hours and tried to make request.
// of course my token is invalid and it will return 401
// so this interceptor checks time and refreshes token immediately before making request
// then continues request with refreshed token
// So I do not get any 401 response. But if this fails and I get 401 then my TokenAuthenticator do his job.
// if my TokenAuthenticator fails too, basically I just logout user and tell him to re-login.
TokenInterceptor tokenInterceptor = new TokenInterceptor();
// this is the critical point that helped me a lot.
// we using only one retrofit instance in our application
// and it uses this dispatcher which can only do 1 request at the same time
// the docs says : Set the maximum number of requests to execute concurrently.
// Above this requests queue in memory, waiting for the running calls to complete.
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(1);
// we are using this OkHttp as client, you can add authenticator, interceptors, dispatchers,
// logging etc. easily for all your requests just editing this OkHttp client
OkHttpClient okClient = new OkHttpClient.Builder()
.connectTimeout(Constants.CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(Constants.READ_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(Constants.WRITE_TIMEOUT, TimeUnit.SECONDS)
.authenticator(tokenAuthenticator)
.addInterceptor(tokenInterceptor)
.dispatcher(dispatcher)
.build();
retrofit = new Retrofit.Builder()
.baseUrl(context.getResources().getString(R.string.base_api_url))
.addConverterFactory(GsonConverterFactory.create(new Gson()))
.client(okClient)
.build();
}
return retrofit;
}
}
Paso 2: En el método de authenticate
mi TokenAuthenticator:
@Override
public Request authenticate(Route route, Response response) throws IOException {
String userRefreshToken="your refresh token";
String cid="your client id";
String csecret="your client secret";
String baseUrl="your base url";
refreshResult=refreshToken(baseUrl,userRefreshToken,cid,csecret);
if (refreshResult) {
//refresh is successful
String newaccess="your new access token";
// make current request with new access token
return response.request().newBuilder()
.header("Authorization", newaccess)
.build();
} else {
// refresh failed , maybe you can logout user
// returning null is critical here, because if you do not return null
// it will try to refresh token continuously like 1000 times.
// also you can try 2-3-4 times by depending you before logging out your user
return null;
}
}
y el método refreshToken
, este es solo un ejemplo, puede crear su propia estrategia al actualizar su token. Estoy usando HttpUrlConnection
porque tengo circunstancias adicionales al actualizar mi token. Mientras tanto te animo a que uses Retrofit
. De todos modos
public boolean refreshToken(String url,String refresh,String cid,String csecret) throws IOException{
URL refreshUrl=new URL(url+"token");
HttpURLConnection urlConnection = (HttpURLConnection) refreshUrl.openConnection();
urlConnection.setDoInput(true);
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
urlConnection.setUseCaches(false);
String urlParameters = "grant_type=refresh_token&client_id="+cid+"&client_secret="+csecret+"&refresh_token="+refresh;
urlConnection.setDoOutput(true);
DataOutputStream wr = new DataOutputStream(urlConnection.getOutputStream());
wr.writeBytes(urlParameters);
wr.flush();
wr.close();
int responseCode = urlConnection.getResponseCode();
if(responseCode==200){
BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// this gson part is optional , you can read response directly from Json too
Gson gson = new Gson();
RefreshTokenResult refreshTokenResult=gson.fromJson(response.toString(),RefreshTokenResult.class);
// handle new token ...
// save it to the sharedpreferences, storage bla bla ...
return true;
} else {
//cannot refresh
return false;
}
}
Paso 3: En realidad lo logramos, pero mostraré un uso simple:
Retrofit client= RetrofitClient.getInstance();
//interface for requests
APIService service = client.create(APIService.class);
// then do your requests .....
Paso 4: Para aquellos que quieren ver la lógica del TokenInterceptor
:
public class TokenInterceptor implements Interceptor{
Context ctx;
SharedPreferences mPrefs;
SharedPreferences.Editor mPrefsEdit;
public TokenInterceptor(Context ctx) {
this.ctx = ctx;
this.mPrefs= PreferenceManager.getDefaultSharedPreferences(ctx);
mPrefsEdit=mPrefs.edit();
}
@Override
public synchronized Response intercept(Chain chain) throws IOException {
Request newRequest=chain.request();
//when saving expire time :
integer expiresIn=response.getExpiresIn();
Calendar c = Calendar.getInstance();
c.add(Calendar.SECOND,expiresIn);
mPrefsEdit.putLong("expiretime",c.getTimeInMillis());
//get expire time from shared preferences
long expireTime=mPrefs.getLong("expiretime",0);
Calendar c = Calendar.getInstance();
Date nowDate=c.getTime();
c.setTimeInMillis(expireTime);
Date expireDate=c.getTime();
int result=nowDate.compareTo(expireDate);
/**
* when comparing dates -1 means date passed so we need to refresh token
* see {@link Date#compareTo}
*/
if(result==-1) {
//refresh token here , and got new access token
String newaccessToken="newaccess";
newRequest=chain.request().newBuilder()
.header("Authorization", newaccessToken)
.build();
}
return chain.proceed(newRequest);
}
}
En mi solicitud, estoy haciendo solicitudes en el servicio de solicitud y en segundo plano. Ambos usan la misma instancia y puedo gestionarlos fácilmente. Por favor, consulte esta respuesta e intente crear su propio cliente. Si todavía tiene problemas, simplemente comente a continuación, mencioneme, incluso otra pregunta, o envíe un correo. Te ayudaré cuando tenga tiempo. Espero que esto ayude.
En tu clase de ApiClient.java :
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new AuthorizationInterceptor(context))
.build();
Agregue la clase TokenManager.java en su paquete de actualización
package co.abc.retrofit;
/**
* Created by ravindrashekhawat on 17/03/17.
*/
public interface TokenManager {
String getToken();
boolean hasToken();
void clearToken();
String refreshToken();
}
Agregue la clase Intercepter en su paquete con el nombre AuthorizationInterceptor.java
package co.smsmagic.retrofit;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;
import com.google.gson.Gson;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import co.abc.models.RefreshTokenResponseModel;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Retrofit;
import retrofit2.http.Header;
import static co.abc.utils.abcConstants.ACCESS_TOKEN;
import static co.abc.utils.abcConstants.BASE_URL;
import static co.abc.utils.abcConstants.GCM_TOKEN;
import static co.abc.utils.abcConstants.JWT_TOKEN_PREFIX;
import static co.abc.utils.abcConstants.REFRESH_TOKEN;
/**
* Created by ravindrashekhawat on 21/03/17.
*/
public class AuthorizationInterceptor implements Interceptor {
private static Retrofit retrofit = null;
private static String deviceToken;
private static String accessToken;
private static String refreshToken;
private static TokenManager tokenManager;
private static Context mContext;
public AuthorizationInterceptor(Context context) {
this.mContext = context;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request modifiedRequest = null;
tokenManager = new TokenManager() {
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
@Override
public String getToken() {
accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
return accessToken;
}
@Override
public boolean hasToken() {
accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
if (accessToken != null && !accessToken.equals("")) {
return true;
}
return false;
}
@Override
public void clearToken() {
sharedPreferences.edit().putString(ACCESS_TOKEN, "").apply();
}
@Override
public String refreshToken() {
final String accessToken = null;
RequestBody reqbody = RequestBody.create(null, new byte[0]);
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(BASE_URL + "refresh")
.method("POST", reqbody)
.addHeader("Authorization", JWT_TOKEN_PREFIX + refreshToken)
.build();
try {
Response response = client.newCall(request).execute();
if ((response.code()) == 200) {
// Get response
String jsonData = response.body().string();
Gson gson = new Gson();
RefreshTokenResponseModel refreshTokenResponseModel = gson.fromJson(jsonData, RefreshTokenResponseModel.class);
if (refreshTokenResponseModel.getRespCode().equals("1")) {
sharedPreferences.edit().putString(ACCESS_TOKEN, refreshTokenResponseModel.getResponse()).apply();
return refreshTokenResponseModel.getResponse();
}
}
} catch (IOException e) {
e.printStackTrace();
}
return accessToken;
}
};
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
deviceToken = sharedPreferences.getString(GCM_TOKEN, "");
accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
refreshToken = sharedPreferences.getString(REFRESH_TOKEN, "");
Response response = chain.proceed(request);
boolean unauthorized =false;
if(response.code() == 401 || response.code() == 422){
unauthorized=true;
}
if (unauthorized) {
tokenManager.clearToken();
tokenManager.refreshToken();
accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
if(accessToken!=null){
modifiedRequest = request.newBuilder()
.addHeader("Authorization", JWT_TOKEN_PREFIX + tokenManager.getToken())
.build();
return chain.proceed(modifiedRequest);
}
}
return response;
}
}
Nota: Este es un código de trabajo para el token de actualización que le proporcioné. Mantenga la calma para cambiar algo de constante, excepto que funcionará perfectamente. Solo intente entender la lógica.
En el fondo hay lógica para llamar de nuevo la misma petición.
if(accessToken!=null){
modifiedRequest = request.newBuilder()
.addHeader("Authorization", JWT_TOKEN_PREFIX + tokenManager.getToken())
.build();
return chain.proceed(modifiedRequest);
}