Android Retrofit2 刷新 Oauth 2 令牌
Android Retrofit2 Refresh Oauth 2 Token
我正在使用 Retrofit
和 OkHttp
库。当我们收到 401
响应时,我有一个 Authenticator
来验证用户。
我的build.gradle
是这样的:
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'
而我的Authenticator
是这样的:
public class CustomAuthanticator implements Authenticator {
@Override
public Request authenticate(Route route, Response response) throws IOException {
//refresh access token
refreshTokenResult=apiService.refreshUserToken(parameters);
//this is synchronous retrofit request
RefreshTokenResult refreshResult = refreshTokenResult.execute().body();
//check if response equals 400, means empty response
if(refreshResult != null) {
// save new access and refresh token
// then create a new request and new access token as header
return response.request().newBuilder()
.header("Authorization", newaccesstoken)
.build();
} else {
// we got empty response and we should return null
// if we don't return null
// this method will try to make so many requests to get new access token
return null;
}
}}
这是我的 APIService
class :
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);
}
我正在使用 Retrofit
这样的:
CustomAuthanticator customAuthanticator=new CustomAuthanticator();
OkHttpClient okClient = new OkHttpClient.Builder()
.authenticator(customAuthanticator)
.build();
Retrofit client = new Retrofit.Builder()
.baseUrl(getResources().getString(R.string.base_api_url))
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okClient)
.build();
//then make retrofit request
所以我的问题是: 有时我会得到一个新的访问令牌并继续工作。但有时我会收到 400
响应,这意味着响应为空。所以我的旧刷新令牌无效,我无法获得新令牌。通常我们的刷新令牌会在 1 年内过期。那么我该怎么做。请帮助我!
Disclaimer :
Actually I am using Dagger
+RxJava
+ Retrofit
but I just wanted to provide an answer to demonstrate logic for future visitors.
Important :
If you are making requests from several places your token will refresh multiple times inside TokenAuthenticator
class. For example when your activity and your service make requests concurrently. To beat this issue just add synchronized
keyword to your TokenAuthenticator
s authenticate
method.
Please make synchronous requests when refreshing your token inside Authenticator
because you must block that thread until your request finishes, otherwise your requests will be executed twice with old and new tokens.
You can use Schedulers.trampoline()
or blockingGet()
when refreshing your token to block that thread.
Also inside authenticate
method you can check if token is already refreshed by comparing request token with stored token to prevent unnecessary refresh.
And please do not consider using TokenInterceptor
because it is edge case and not for everyone, just focus on TokenAuthenticator
.
这就是我们正在努力实现的目标:
首先刷新令牌对于大多数应用程序来说是一个关键过程。
流程是:如果刷新令牌失败,注销当前用户并要求重新登录。 (也许在注销用户之前重试几次刷新令牌)
反正我会一步步解释的:
第 1 步: 请参考 singleton pattern,我们将创建一个 class 负责返回我们的改造实例。因为如果没有可用的实例它是静态的,它只创建一次实例并且当你调用它时总是 returns 这个静态实例。这也是单例设计模式的基本定义。
public class RetrofitClient {
private static Retrofit retrofit = null;
private RetrofitClient() {
// private constructor to prevent access
// only way to access: Retrofit client = RetrofitClient.getInstance();
}
public static Retrofit getInstance() {
if (retrofit == null) {
// TokenAuthenticator can be singleton too
TokenAuthenticator tokenAuthenticator = new TokenAuthenticator();
// !! This interceptor is not required for everyone !!
// Main purpose of this interceptor is to reduce server calls
// Our token needs to be refreshed after 10 hours
// We open our app after 50 hours and try to make a request.
// Of course token is expired and we will get a 401 response.
// So this interceptor checks time and refreshes token beforehand.
// If this fails and I get 401 then my TokenAuthenticator does its job.
// if my TokenAuthenticator fails too, basically I just logout the user.
TokenInterceptor tokenInterceptor = new TokenInterceptor();
OkHttpClient okClient = new OkHttpClient.Builder()
.authenticator(tokenAuthenticator)
.addInterceptor(tokenInterceptor)
.build();
retrofit = new Retrofit.Builder()
.baseUrl(base_api_url)
.client(okClient)
.build();
}
return retrofit;
}
}
步骤 2: 在我的 TokenAuthenticator 的 authenticate
方法中:
@Override
public synchronized Request authenticate(Route route, Response response) throws IOException {
boolean refreshResult = refreshToken();
if (refreshResult) {
// refresh token is successful, we saved new token to storage.
// Get your token from storage and set header
String newaccesstoken = "your new access token";
// execute failed request again with new access token
return response.request().newBuilder()
.header("Authorization", newaccesstoken)
.build();
} else {
// Refresh token failed, you can logout user or retry couple of times
// Returning null is critical here, it will stop the current request
// If you do not return null, you will end up in a loop calling refresh
return null;
}
}
和refreshToken
方法,这只是您可以创建自己的示例:
public boolean refreshToken() {
// you can use RxJava with Retrofit and add blockingGet
// it is up to you how to refresh your token
RefreshTokenResult result = retrofit.refreshToken();
int responseCode = result.getResponseCode();
if(responseCode == 200) {
// save new token to sharedpreferences, storage etc.
return true;
} else {
//cannot refresh
return false;
}
}
第三步:想看TokenInterceptor
逻辑的朋友:
public class TokenInterceptor implements Interceptor {
SharedPreferences prefs;
SharedPreferences.Editor prefsEdit;
@Override
public Response intercept(Chain chain) throws IOException {
Request newRequest = chain.request();
// get expire time from shared preferences
long expireTime = prefs.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
if(result == -1) {
//refresh token here , and get new access token
TokenResponse tokenResponse = refreshToken();
// Save refreshed token's expire time :
integer expiresIn = tokenResponse.getExpiresIn();
Calendar c = Calendar.getInstance();
c.add(Calendar.SECOND,expiresIn);
prefsEdit.putLong("expiretime",c.getTimeInMillis());
String newaccessToken = "new access token";
newRequest=chain.request().newBuilder()
.header("Authorization", newaccessToken)
.build();
}
return chain.proceed(newRequest);
}
}
我在活动和后台服务中提出请求。他们都使用相同的改造实例,我可以轻松管理访问令牌。请参考此答案并尝试创建您自己的客户端。如果您仍有问题,请在下方评论,我会尽力提供帮助。
在你的 ApiClient.java class :
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new AuthorizationInterceptor(context))
.build();
在您的改造包中添加TokenManager.javaclass
package co.abc.retrofit;
/**
* Created by ravindrashekhawat on 17/03/17.
*/
public interface TokenManager {
String getToken();
boolean hasToken();
void clearToken();
String refreshToken();
}
在您的包中添加拦截器 class,名称为 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;
}
}
注意: 这是我提供的刷新令牌的工作代码,请保持冷静,您只需更改一些常量即可,但它会起作用 perfectly.Just 尝试理解逻辑 .
在底部有再次调用相同请求的逻辑
if(accessToken!=null){
modifiedRequest = request.newBuilder()
.addHeader("Authorization", JWT_TOKEN_PREFIX + tokenManager.getToken())
.build();
return chain.proceed(modifiedRequest);
}
我正在使用 Retrofit
和 OkHttp
库。当我们收到 401
响应时,我有一个 Authenticator
来验证用户。
我的build.gradle
是这样的:
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'
而我的Authenticator
是这样的:
public class CustomAuthanticator implements Authenticator {
@Override
public Request authenticate(Route route, Response response) throws IOException {
//refresh access token
refreshTokenResult=apiService.refreshUserToken(parameters);
//this is synchronous retrofit request
RefreshTokenResult refreshResult = refreshTokenResult.execute().body();
//check if response equals 400, means empty response
if(refreshResult != null) {
// save new access and refresh token
// then create a new request and new access token as header
return response.request().newBuilder()
.header("Authorization", newaccesstoken)
.build();
} else {
// we got empty response and we should return null
// if we don't return null
// this method will try to make so many requests to get new access token
return null;
}
}}
这是我的 APIService
class :
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);
}
我正在使用 Retrofit
这样的:
CustomAuthanticator customAuthanticator=new CustomAuthanticator();
OkHttpClient okClient = new OkHttpClient.Builder()
.authenticator(customAuthanticator)
.build();
Retrofit client = new Retrofit.Builder()
.baseUrl(getResources().getString(R.string.base_api_url))
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okClient)
.build();
//then make retrofit request
所以我的问题是: 有时我会得到一个新的访问令牌并继续工作。但有时我会收到 400
响应,这意味着响应为空。所以我的旧刷新令牌无效,我无法获得新令牌。通常我们的刷新令牌会在 1 年内过期。那么我该怎么做。请帮助我!
Disclaimer : Actually I am using
Dagger
+RxJava
+Retrofit
but I just wanted to provide an answer to demonstrate logic for future visitors.
Important : If you are making requests from several places your token will refresh multiple times inside
TokenAuthenticator
class. For example when your activity and your service make requests concurrently. To beat this issue just addsynchronized
keyword to yourTokenAuthenticator
sauthenticate
method.
Please make synchronous requests when refreshing your token inside
Authenticator
because you must block that thread until your request finishes, otherwise your requests will be executed twice with old and new tokens. You can useSchedulers.trampoline()
orblockingGet()
when refreshing your token to block that thread.
Also inside
authenticate
method you can check if token is already refreshed by comparing request token with stored token to prevent unnecessary refresh.
And please do not consider using
TokenInterceptor
because it is edge case and not for everyone, just focus onTokenAuthenticator
.
这就是我们正在努力实现的目标:
首先刷新令牌对于大多数应用程序来说是一个关键过程。 流程是:如果刷新令牌失败,注销当前用户并要求重新登录。 (也许在注销用户之前重试几次刷新令牌)
反正我会一步步解释的:
第 1 步: 请参考 singleton pattern,我们将创建一个 class 负责返回我们的改造实例。因为如果没有可用的实例它是静态的,它只创建一次实例并且当你调用它时总是 returns 这个静态实例。这也是单例设计模式的基本定义。
public class RetrofitClient {
private static Retrofit retrofit = null;
private RetrofitClient() {
// private constructor to prevent access
// only way to access: Retrofit client = RetrofitClient.getInstance();
}
public static Retrofit getInstance() {
if (retrofit == null) {
// TokenAuthenticator can be singleton too
TokenAuthenticator tokenAuthenticator = new TokenAuthenticator();
// !! This interceptor is not required for everyone !!
// Main purpose of this interceptor is to reduce server calls
// Our token needs to be refreshed after 10 hours
// We open our app after 50 hours and try to make a request.
// Of course token is expired and we will get a 401 response.
// So this interceptor checks time and refreshes token beforehand.
// If this fails and I get 401 then my TokenAuthenticator does its job.
// if my TokenAuthenticator fails too, basically I just logout the user.
TokenInterceptor tokenInterceptor = new TokenInterceptor();
OkHttpClient okClient = new OkHttpClient.Builder()
.authenticator(tokenAuthenticator)
.addInterceptor(tokenInterceptor)
.build();
retrofit = new Retrofit.Builder()
.baseUrl(base_api_url)
.client(okClient)
.build();
}
return retrofit;
}
}
步骤 2: 在我的 TokenAuthenticator 的 authenticate
方法中:
@Override
public synchronized Request authenticate(Route route, Response response) throws IOException {
boolean refreshResult = refreshToken();
if (refreshResult) {
// refresh token is successful, we saved new token to storage.
// Get your token from storage and set header
String newaccesstoken = "your new access token";
// execute failed request again with new access token
return response.request().newBuilder()
.header("Authorization", newaccesstoken)
.build();
} else {
// Refresh token failed, you can logout user or retry couple of times
// Returning null is critical here, it will stop the current request
// If you do not return null, you will end up in a loop calling refresh
return null;
}
}
和refreshToken
方法,这只是您可以创建自己的示例:
public boolean refreshToken() {
// you can use RxJava with Retrofit and add blockingGet
// it is up to you how to refresh your token
RefreshTokenResult result = retrofit.refreshToken();
int responseCode = result.getResponseCode();
if(responseCode == 200) {
// save new token to sharedpreferences, storage etc.
return true;
} else {
//cannot refresh
return false;
}
}
第三步:想看TokenInterceptor
逻辑的朋友:
public class TokenInterceptor implements Interceptor {
SharedPreferences prefs;
SharedPreferences.Editor prefsEdit;
@Override
public Response intercept(Chain chain) throws IOException {
Request newRequest = chain.request();
// get expire time from shared preferences
long expireTime = prefs.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
if(result == -1) {
//refresh token here , and get new access token
TokenResponse tokenResponse = refreshToken();
// Save refreshed token's expire time :
integer expiresIn = tokenResponse.getExpiresIn();
Calendar c = Calendar.getInstance();
c.add(Calendar.SECOND,expiresIn);
prefsEdit.putLong("expiretime",c.getTimeInMillis());
String newaccessToken = "new access token";
newRequest=chain.request().newBuilder()
.header("Authorization", newaccessToken)
.build();
}
return chain.proceed(newRequest);
}
}
我在活动和后台服务中提出请求。他们都使用相同的改造实例,我可以轻松管理访问令牌。请参考此答案并尝试创建您自己的客户端。如果您仍有问题,请在下方评论,我会尽力提供帮助。
在你的 ApiClient.java class :
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new AuthorizationInterceptor(context))
.build();
在您的改造包中添加TokenManager.javaclass
package co.abc.retrofit;
/**
* Created by ravindrashekhawat on 17/03/17.
*/
public interface TokenManager {
String getToken();
boolean hasToken();
void clearToken();
String refreshToken();
}
在您的包中添加拦截器 class,名称为 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;
}
}
注意: 这是我提供的刷新令牌的工作代码,请保持冷静,您只需更改一些常量即可,但它会起作用 perfectly.Just 尝试理解逻辑 .
在底部有再次调用相同请求的逻辑
if(accessToken!=null){
modifiedRequest = request.newBuilder()
.addHeader("Authorization", JWT_TOKEN_PREFIX + tokenManager.getToken())
.build();
return chain.proceed(modifiedRequest);
}