在使用 Volley 重试旧请求之前重试时获取新令牌
Getting new token on retry before retrying old request with Volley
我有一个使用 Volley 实现的简单身份验证系统。它是这样的:
登录时从服务器获取令牌 -> 一小时后,此令牌过期 -> 过期时,我们会在失败的 API 调用中发现它,因此我们应该(重试) -> 获取新令牌当那个调用失败然后 -> 重试原来的调用。
我已经实现了这个,并且令牌成功返回,但是因为我认为我对 Volley RequestQueue 做错了什么,原始请求在新的有效令牌能够用过的。请看以下代码:
public class GeneralAPICall extends Request<JSONObject> {
public static String LOG_TAG = GeneralAPICall.class.getSimpleName();
SessionManager sessionManager; //instance of sessionManager needed to get user's credentials
private Response.Listener<JSONObject> listener; //the response listener used to deliver the response
private Map<String, String> headers = new HashMap<>(); //the headers used to authenticate
private Map<String, String> params; //the params to pass with API call, can be null
public GeneralAPICall(int method, String url, Map<String, String> params, Context context, Response.Listener<JSONObject> responseListener, Response.ErrorListener errorListener) {
super(method, url, errorListener);
sessionManager = new SessionManager(context); //instantiate
HashMap<String, String> credentials = sessionManager.getUserDetails(); //get the user's credentials for authentication
this.listener = responseListener;
this.params = params;
//encode the user's username and token
String loginEncoded = new String(Base64.encode((credentials.get(Constants.SessionManagerConstants.KEY_USERNAME)
+ Constants.APIConstants.Characters.CHAR_COLON
+ credentials.get(Constants.SessionManagerConstants.KEY_TOKEN)).getBytes(), Base64.NO_WRAP));
Log.v(LOG_TAG, loginEncoded); //TODO: remove
this.headers.put(Constants.APIConstants.BasicAuth.AUTHORIZATION, Constants.APIConstants.BasicAuth.BASIC + loginEncoded); //set the encoded information as the header
setRetryPolicy(new TokenRetryPolicy(context)); //**THE RETRY POLICY**
}
我设置的重试策略定义为默认,但我实现了自己的重试方法:
@Override
public void retry(VolleyError error) throws VolleyError {
Log.v(LOG_TAG, "Initiating a retry");
mCurrentRetryCount++; //increment our retry count
mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
if (error instanceof AuthFailureError) { //we got a 401, and need a new token
Log.v(LOG_TAG, "AuthFailureError found!");
VolleyUser.refreshTokenTask(context, this); //**GET A NEW TOKEN**
}
if (!hasAttemptRemaining()) {
Log.v(LOG_TAG, "No attempt remaining, ERROR");
throw error;
}
}
刷新令牌任务定义了一个刷新API调用
public static void refreshTokenTask(Context context, IRefreshTokenReturn listener) {
Log.v(LOG_TAG, "refresh token task called");
final IRefreshTokenReturn callBack = listener;
RefreshAPICall request = new RefreshAPICall(Request.Method.GET, Constants.APIConstants.URL.GET_TOKEN_URL, context, new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
String token = response.getString(Constants.APIConstants.Returns.RETURN_TOKEN);
Log.v(LOG_TAG, "Token from return is: " + token);
callBack.onTokenRefreshComplete(token);
} catch (JSONException e) {
callBack.onTokenRefreshComplete(null); //TODO: log this
e.printStackTrace();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.v(LOG_TAG, "Error with RETRY : " + error.toString());
}
});
VolleySingleton.getInstance(context).addToRequestQueue(request);
}
我们的刷新API调用定义:
public RefreshAPICall(int method, String url, Context context, Response.Listener<JSONObject> responseListener, Response.ErrorListener errorListener) {
super(method, url, errorListener);
sessionManager = new SessionManager(context); //instantiate
HashMap<String, String> credentials = sessionManager.getRefreshUserDetails(); //get the user's credentials for authentication
this.listener = responseListener;
//encode the user's username and token
String loginEncoded = new String(Base64.encode((credentials.get(Constants.SessionManagerConstants.KEY_USERNAME)
+ Constants.APIConstants.Characters.CHAR_COLON
+ credentials.get(Constants.SessionManagerConstants.KEY_PASSWORD)).getBytes(), Base64.NO_WRAP));
this.headers.put(Constants.APIConstants.BasicAuth.AUTHORIZATION, Constants.APIConstants.BasicAuth.BASIC + loginEncoded); //set the encoded information as the header
setTag(Constants.VolleyConstants.RETRY_TAG); //mark the retry calls with a tag so we can delete any others once we get a new token
setPriority(Priority.IMMEDIATE); //set priority as immediate because this needs to be done before anything else
//debug lines
Log.v(LOG_TAG, "RefreshAPICall made with " + credentials.get(Constants.SessionManagerConstants.KEY_USERNAME) + " " +
credentials.get(Constants.SessionManagerConstants.KEY_PASSWORD));
Log.v(LOG_TAG, "Priority set on refresh call is " + getPriority());
Log.v(LOG_TAG, "Tag for Call is " + getTag());
}
我将此请求的优先级设置为高,以便它在失败的请求之前被触发,因此一旦我们获得令牌,原始调用就可以使用有效令牌触发。
最后,根据响应,我删除了带有重试标签的任何其他任务(以防多次 API 调用失败并进行多次重试调用,我们不想多次覆盖新令牌)
@Override
public void onTokenRefreshComplete(String token) {
VolleySingleton.getInstance(context).getRequestQueue().cancelAll(Constants.VolleyConstants.RETRY_TAG);
Log.v(LOG_TAG, "Cancelled all retry calls");
SessionManager sessionManager = new SessionManager(context);
sessionManager.setStoredToken(token);
Log.v(LOG_TAG, "Logged new token");
}
不幸的是,LogCat 告诉我所有重试都在我们使用令牌之前发生。令牌成功返回,但很明显 IMMEDIATE 优先级对队列调度调用的顺序没有影响。
任何有关如何确保我的刷新API调用在其他任务之前被触发的帮助将不胜感激。我想知道 Volley 是否将 RefreshAPICall 视为原始失败任务的子任务,因此它会尝试调用该原始任务的重试次数,直到重试结束,然后触发 RefreshAPI呼叫.
LogCat(不确定如何让它看起来漂亮):
05-05 16:12:07.145: E/Volley(1972): [137] BasicNetwork.performRequest:
Unexpected response code **401 for https://url.me/api/get_friends**
05-05 16:12:07.145: V/TokenRetryPolicy(1972): Initiating a retry
05-05 16:12:07.145: V/TokenRetryPolicy(1972): AuthFailureError found!
05-05 16:12:07.146: V/VolleyUser(1972): refresh token task called
05-05 16:12:07.146: V/RefreshAPICall(1972): RefreshAPICall made with username user_password
05-05 16:12:07.147: V/RefreshAPICall(1972): Priority set on refresh call is HIGH
05-05 16:12:07.147: V/RefreshAPICall(1972): Tag for Call is retry
05-05 16:12:07.265: E/Volley(1972): [137] BasicNetwork.performRequest: Unexpected response code **401 for https://url.me/api/get_friends**
05-05 16:12:07.265: V/TokenRetryPolicy(1972): Initiating a retry
05-05 16:12:07.265: V/TokenRetryPolicy(1972): AuthFailureError found!
05-05 16:12:07.265: V/VolleyUser(1972): refresh token task called
05-05 16:12:07.265: V/RefreshAPICall(1972): RefreshAPICall made with user user_password
05-05 16:12:07.265: V/RefreshAPICall(1972): Priority set on refresh call is HIGH
05-05 16:12:07.265: V/RefreshAPICall(1972): Tag for Call is retry
05-05 16:12:07.265: V/TokenRetryPolicy(1972): No attempt remaining, ERROR
05-05 16:12:08.219: I/Choreographer(1972): Skipped 324 frames! The application may be doing too much work on its main thread.
05-05 16:12:08.230: V/RefreshAPICall(1972): Response from server on refresh is: {"status":"success","token":"d5792e18c0e1acb3ad507dbae854eb2cdc5962a2c1b610a6b77e3bc3033c7f64"}
05-05 16:12:08.230: V/VolleyUser(1972): Token from return is: d5792e18c0e1acb3ad507dbae854eb2cdc5962a2c1b610a6b77e3bc3033c7f64
05-05 16:12:08.231: V/TokenRetryPolicy(1972): Cancelled all retry calls
05-05 16:12:08.257: V/SessionManager(1972): New Token In SharedPref is: d5792e18c0e1acb3ad507dbae854eb2cdc5962a2c1b610a6b77e3bc3033c7f64
05-05 16:12:08.257: V/TokenRetryPolicy(1972): Logged new token
现在发布一个答案,因为我找到了一种在重试时处理令牌刷新的半正经方法。
当我使用 Volley 创建我的一般(最常见)API 调用时,我会保存对调用的引用以防它失败,并将其传递给我的重试策略。
public GeneralAPICall(int method, String url, Map<String, String> params, Context context, Response.Listener<JSONObject> responseListener, Response.ErrorListener errorListener) {
super(method, url, errorListener);
sessionManager = SessionManager.getmInstance(context);
HashMap<String, String> credentials = sessionManager.getUserDetails(); // Get the user's credentials for authentication
this.listener = responseListener;
this.params = params;
// Encode the user's username and token
String loginEncoded = new String(Base64.encode((credentials.get(Constants.SessionManagerConstants.KEY_USERNAME)
+ Constants.APIConstants.Characters.CHAR_COLON
+ credentials.get(Constants.SessionManagerConstants.KEY_TOKEN)).getBytes(), Base64.NO_WRAP));
this.headers.put(Constants.APIConstants.BasicAuth.AUTHORIZATION, Constants.APIConstants.BasicAuth.BASIC + loginEncoded); // Set the encoded information as the header
setRetryPolicy(new TokenRetryPolicy(context, this)); //passing "this" saves the reference
}
然后,在我的重试策略 class 中(它只是扩展了 DefaultRetryPolicy,当我收到 401 错误告诉我需要一个新令牌时,我发出 refreshToken 调用以获取一个新令牌。
public class TokenRetryPolicy extends DefaultRetryPolicy implements IRefreshTokenReturn{
...
@Override
public void retry(VolleyError error) throws VolleyError {
mCurrentRetryCount++; //increment our retry count
mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
if (error instanceof AuthFailureError && sessionManager.isLoggedIn()) {
mCurrentRetryCount = mMaxNumRetries + 1; // Don't retry anymore, it's pointless
VolleyUser.refreshTokenTask(context, this); // Get new token
} if (!hasAttemptRemaining()) {
Log.v(LOG_TAG, "No attempt remaining, ERROR");
throw error;
}
}
...
}
调用 returns 后,我会在重试策略 class 中处理响应。我修改了失败的调用,给它新的令牌(在将令牌存储在 SharedPrefs 之后)来验证自己,然后再次启动它!
@Override
public void onTokenRefreshComplete(String token, String expiration) {
sessionManager.setStoredToken(token, expiration);
HashMap<String, String> credentials = sessionManager.getUserDetails(); //get the user's credentials for authentication
//encode the user's username and token
String loginEncoded = new String(Base64.encode((credentials.get(Constants.SessionManagerConstants.KEY_USERNAME)
+ Constants.APIConstants.Characters.CHAR_COLON
+ credentials.get(Constants.SessionManagerConstants.KEY_TOKEN)).getBytes(), Base64.NO_WRAP));
Log.v(LOG_TAG, loginEncoded); //TODO: remove
callThatFailed.setHeaders(Constants.APIConstants.BasicAuth.AUTHORIZATION, Constants.APIConstants.BasicAuth.BASIC + loginEncoded); //modify "old, failed" call - set the encoded information as the header
VolleySingleton.getInstance(context).getRequestQueue().add(callThatFailed);
Log.v(LOG_TAG, "fired off new call");
}
这个实现非常适合我。
但是,我应该注意这种情况应该不会发生太多,因为我了解到我应该在[=29=之前检查我的令牌是否已过期] 进行任何 API 调用。这可以通过在 SharedPrefs 中存储过期时间(从服务器返回)并查看是否 current_time - 过期时间 < some_time 来实现,其中 some_time 是您想要的时间量在它过期之前获得一个新令牌,对我来说是 10 秒。
希望这对那里的人有所帮助,如果我有任何错误,请发表评论!
我知道这个 post 这么老了,但是 post 在其他建议的解决方案之后我的解决方案对我没有帮助。
注意 - 我确实尝试了上面给出的 Brandon 的方法,即扩展 DefaultRetryPolicy。但它的字段是私有的,所以不想实现整个 class,必须有更好的方法。
所以我把代码写在CustomRequest class extending Request.以下是相关片段 -
在登录响应中存储令牌 -
@Override
protected Response<T> parseNetworkResponse(NetworkResponse response) {
...
//if oauth data is sent with response, store in SharedPrefs
...
}
如果访问令牌已过期 -
@Override
protected VolleyError parseNetworkError(VolleyError volleyError) {
...
if (volleyError instanceof NoConnectionError) {
//i know, there has to be a better way than checking this.
//will work on it later
if(volleyError.getMessage().equalsIgnoreCase("java.io.IOException: No authentication challenges found")) {
String accessToken = getNewAccessToken();//synchronous call
//retry
if(accessToken != null) {
//IMP: this is the statement which will retry the request manually
NetworkHelper.get(mContext).getRequestQueue().add(this);
}
}
}
...
}
将访问令牌附加到请求 -
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
...
String accesssToken = //get from SharedPrefs
headers.put("Authorization", "Bearer " +accessToken);
...
}
如果刷新令牌无效,将转到登录屏幕 -
private void showLogin(){
//stop all current requests
//cancelAllRequests();
Intent intent = new Intent(mContext, LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
mContext.startActivity(intent);
}
正在使用刷新令牌获取新的访问令牌。这必须是使用 RequestFuture -
的同步方法
private String getNewAccessToken(){
...
//get new access token from server and store in SharedPrefs
...
//also return the new token so that we know if we need to retry or not
return newAccessToken;
}
HTH
我现在使用的策略是在失败重试的时候加上一个refreshToken。这是自定义失败重试。
public class CustomRetryPolicy implements RetryPolicy
{
private static final String TAG = "Refresh";
private Request request;
/**
* The current timeout in milliseconds.
*/
private int mCurrentTimeoutMs;
/**
* The current retry count.
*/
private int mCurrentRetryCount;
/**
* The maximum number of attempts.
*/
private final int mMaxNumRetries;
/**
* The backoff multiplier for the policy.
*/
private final float mBackoffMultiplier;
/**
* The default socket timeout in milliseconds
*/
public static final int DEFAULT_TIMEOUT_MS = 2500;
/**
* The default number of retries
*/
public static final int DEFAULT_MAX_RETRIES = 1;
/**
* The default backoff multiplier
*/
public static final float DEFAULT_BACKOFF_MULT = 1f;
/**
* Constructs a new retry policy using the default timeouts.
*/
public CustomRetryPolicy() {
this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
}
/**
* Constructs a new retry policy.
*
* @param initialTimeoutMs The initial timeout for the policy.
* @param maxNumRetries The maximum number of retries.
* @param backoffMultiplier Backoff multiplier for the policy.
*/
public CustomRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) {
mCurrentTimeoutMs = initialTimeoutMs;
mMaxNumRetries = maxNumRetries;
mBackoffMultiplier = backoffMultiplier;
}
/**
* Returns the current timeout.
*/
@Override
public int getCurrentTimeout() {
return mCurrentTimeoutMs;
}
/**
* Returns the current retry count.
*/
@Override
public int getCurrentRetryCount() {
return mCurrentRetryCount;
}
/**
* Returns the backoff multiplier for the policy.
*/
public float getBackoffMultiplier() {
return mBackoffMultiplier;
}
/**
* Prepares for the next retry by applying a backoff to the timeout.
*
* @param error The error code of the last attempt.
*/
@SuppressWarnings("unchecked")
@Override
public void retry(VolleyError error) throws VolleyError {
mCurrentRetryCount++;
mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
if (!hasAttemptRemaining()) {
throw error;
}
//401 and 403
if (error instanceof AuthFailureError) {//Just token invalid,refresh token
AuthFailureError er = (AuthFailureError) error;
if (er.networkResponse != null && er.networkResponse.statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
//Count is used to reset the flag
RefreshTokenManager instance = RefreshTokenManager.getInstance();
instance.increaseCount();
CUtils.logD(TAG, "come retry count: " + instance.getCount());
boolean ok = instance.refreshToken();
if (ok) {
Map<String, String> headers = request.getHeaders();
String[] tokens = instance.getTokens();
headers.put("token", tokens[0]);
Log.d(TAG, "retry:success");
} else {
throw error;
}
}
}
}
/**
* Returns true if this policy has attempts remaining, false otherwise.
*/
protected boolean hasAttemptRemaining() {
return mCurrentRetryCount <= mMaxNumRetries;
}
public Request getRequest() {
return request;
}
public void setRequest(Request request) {
this.request = request;
}
}
刷新令牌
public class RefreshTokenManager {
private static final String TAG = "Refresh";
private static RefreshTokenManager instance;
private final RefreshFlag flag;
/**
*retry count
*/
private AtomicInteger count = new AtomicInteger();
public int getCount() {
return count.get();
}
public int increaseCount() {
return count.getAndIncrement();
}
public void resetCount() {
this.count.set(0);
}
/**
* 锁
*/
private Lock lock;
public static RefreshTokenManager getInstance() {
synchronized (RefreshTokenManager.class) {
if (instance == null) {
synchronized (RefreshTokenManager.class) {
instance = new RefreshTokenManager();
}
}
}
return instance;
}
private RefreshTokenManager() {
flag = new RefreshFlag();
lock = new ReentrantLock();
}
public void resetFlag() {
lock.lock();
RefreshFlag flag = getFlag();
flag.resetFlag();
lock.unlock();
}
protected boolean refreshToken() {
lock.lock();
RefreshFlag flag = getFlag();
//Reset the flag so that the next time the token fails, it can enter normally.
if (flag.isFailure()) {
if (count.decrementAndGet() == 0) {
resetFlag();
}
lock.unlock();
return false;
} else if (flag.isSuccess()) {
CUtils.logD(TAG, "decrease retry count: " + instance.getCount());
if (count.decrementAndGet() == 0) {
count.incrementAndGet();
flag.resetFlag();
} else {
lock.unlock();
return true;
}
}
// refreshToken is doing.
flag.setDoing();
//Upload refresh_token and get the response from the server
String response = postRefreshTokenRequest();
CUtils.logD(TAG, "refreshToken: response " + response);
if (!TextUtils.isEmpty(response)) {
try {
JSONObject jsonObject = new JSONObject(response);
JSONObject data = jsonObject.optJSONObject("data");
if (data != null) {
String token = data.optString("token");
String refreshToken = data.optString("refresh_token");
CUtils.logD(TAG, "refreshToken: token : " + token + "\n" + "refresh_token : " + refreshToken);
if (!TextUtils.isEmpty(token) && !TextUtils.isEmpty(refreshToken)) {
//success,save token and refresh_token
saveTokens(token, refreshToken);
CUtils.logD(TAG, "run: success notify ");
flag.setSuccess();
if (count.decrementAndGet() == 0) {
resetFlag();
}
CUtils.logD(TAG, "decrease retry count: " + instance.getCount());
lock.unlock();
return true;
}
}
} catch (Exception e) {
CUtils.logE(e);
}
}
//delete local token and refresh_token
removeTokens();
flag.setFailure();
count.decrementAndGet();
CUtils.logD(TAG, "decrease retry count: " + instance.getCount());
lock.unlock();
CUtils.logD(TAG, "run: fail notify ");
return false;
}
private RefreshFlag getFlag() {
return flag;
}
}
这是旗帜
public final class RefreshFlag {
private static final int FLAG_SUCCESS = 0x01;
private static final int FLAG_DOING = 0x11;
private static final int FLAG_FAILURE = 0x10;
private static final int FLAG_INIT = 0x00;
/**
* flag 标志位
*/
private int flag = FLAG_INIT;
public boolean isDoingLocked() {
return flag == FLAG_DOING;
}
public void setDoing() {
flag = FLAG_DOING;
}
public void setSuccess() {
flag = FLAG_SUCCESS;
}
public void setFailure() {
flag = FLAG_FAILURE;
}
public boolean isSuccess() {
return flag == FLAG_SUCCESS;
}
public boolean isFailure() {
return flag == FLAG_FAILURE;
}
public void resetFlag() {
flag = FLAG_INIT;
}
}
我有一个使用 Volley 实现的简单身份验证系统。它是这样的: 登录时从服务器获取令牌 -> 一小时后,此令牌过期 -> 过期时,我们会在失败的 API 调用中发现它,因此我们应该(重试) -> 获取新令牌当那个调用失败然后 -> 重试原来的调用。
我已经实现了这个,并且令牌成功返回,但是因为我认为我对 Volley RequestQueue 做错了什么,原始请求在新的有效令牌能够用过的。请看以下代码:
public class GeneralAPICall extends Request<JSONObject> {
public static String LOG_TAG = GeneralAPICall.class.getSimpleName();
SessionManager sessionManager; //instance of sessionManager needed to get user's credentials
private Response.Listener<JSONObject> listener; //the response listener used to deliver the response
private Map<String, String> headers = new HashMap<>(); //the headers used to authenticate
private Map<String, String> params; //the params to pass with API call, can be null
public GeneralAPICall(int method, String url, Map<String, String> params, Context context, Response.Listener<JSONObject> responseListener, Response.ErrorListener errorListener) {
super(method, url, errorListener);
sessionManager = new SessionManager(context); //instantiate
HashMap<String, String> credentials = sessionManager.getUserDetails(); //get the user's credentials for authentication
this.listener = responseListener;
this.params = params;
//encode the user's username and token
String loginEncoded = new String(Base64.encode((credentials.get(Constants.SessionManagerConstants.KEY_USERNAME)
+ Constants.APIConstants.Characters.CHAR_COLON
+ credentials.get(Constants.SessionManagerConstants.KEY_TOKEN)).getBytes(), Base64.NO_WRAP));
Log.v(LOG_TAG, loginEncoded); //TODO: remove
this.headers.put(Constants.APIConstants.BasicAuth.AUTHORIZATION, Constants.APIConstants.BasicAuth.BASIC + loginEncoded); //set the encoded information as the header
setRetryPolicy(new TokenRetryPolicy(context)); //**THE RETRY POLICY**
}
我设置的重试策略定义为默认,但我实现了自己的重试方法:
@Override
public void retry(VolleyError error) throws VolleyError {
Log.v(LOG_TAG, "Initiating a retry");
mCurrentRetryCount++; //increment our retry count
mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
if (error instanceof AuthFailureError) { //we got a 401, and need a new token
Log.v(LOG_TAG, "AuthFailureError found!");
VolleyUser.refreshTokenTask(context, this); //**GET A NEW TOKEN**
}
if (!hasAttemptRemaining()) {
Log.v(LOG_TAG, "No attempt remaining, ERROR");
throw error;
}
}
刷新令牌任务定义了一个刷新API调用
public static void refreshTokenTask(Context context, IRefreshTokenReturn listener) {
Log.v(LOG_TAG, "refresh token task called");
final IRefreshTokenReturn callBack = listener;
RefreshAPICall request = new RefreshAPICall(Request.Method.GET, Constants.APIConstants.URL.GET_TOKEN_URL, context, new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
String token = response.getString(Constants.APIConstants.Returns.RETURN_TOKEN);
Log.v(LOG_TAG, "Token from return is: " + token);
callBack.onTokenRefreshComplete(token);
} catch (JSONException e) {
callBack.onTokenRefreshComplete(null); //TODO: log this
e.printStackTrace();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.v(LOG_TAG, "Error with RETRY : " + error.toString());
}
});
VolleySingleton.getInstance(context).addToRequestQueue(request);
}
我们的刷新API调用定义:
public RefreshAPICall(int method, String url, Context context, Response.Listener<JSONObject> responseListener, Response.ErrorListener errorListener) {
super(method, url, errorListener);
sessionManager = new SessionManager(context); //instantiate
HashMap<String, String> credentials = sessionManager.getRefreshUserDetails(); //get the user's credentials for authentication
this.listener = responseListener;
//encode the user's username and token
String loginEncoded = new String(Base64.encode((credentials.get(Constants.SessionManagerConstants.KEY_USERNAME)
+ Constants.APIConstants.Characters.CHAR_COLON
+ credentials.get(Constants.SessionManagerConstants.KEY_PASSWORD)).getBytes(), Base64.NO_WRAP));
this.headers.put(Constants.APIConstants.BasicAuth.AUTHORIZATION, Constants.APIConstants.BasicAuth.BASIC + loginEncoded); //set the encoded information as the header
setTag(Constants.VolleyConstants.RETRY_TAG); //mark the retry calls with a tag so we can delete any others once we get a new token
setPriority(Priority.IMMEDIATE); //set priority as immediate because this needs to be done before anything else
//debug lines
Log.v(LOG_TAG, "RefreshAPICall made with " + credentials.get(Constants.SessionManagerConstants.KEY_USERNAME) + " " +
credentials.get(Constants.SessionManagerConstants.KEY_PASSWORD));
Log.v(LOG_TAG, "Priority set on refresh call is " + getPriority());
Log.v(LOG_TAG, "Tag for Call is " + getTag());
}
我将此请求的优先级设置为高,以便它在失败的请求之前被触发,因此一旦我们获得令牌,原始调用就可以使用有效令牌触发。
最后,根据响应,我删除了带有重试标签的任何其他任务(以防多次 API 调用失败并进行多次重试调用,我们不想多次覆盖新令牌)
@Override
public void onTokenRefreshComplete(String token) {
VolleySingleton.getInstance(context).getRequestQueue().cancelAll(Constants.VolleyConstants.RETRY_TAG);
Log.v(LOG_TAG, "Cancelled all retry calls");
SessionManager sessionManager = new SessionManager(context);
sessionManager.setStoredToken(token);
Log.v(LOG_TAG, "Logged new token");
}
不幸的是,LogCat 告诉我所有重试都在我们使用令牌之前发生。令牌成功返回,但很明显 IMMEDIATE 优先级对队列调度调用的顺序没有影响。
任何有关如何确保我的刷新API调用在其他任务之前被触发的帮助将不胜感激。我想知道 Volley 是否将 RefreshAPICall 视为原始失败任务的子任务,因此它会尝试调用该原始任务的重试次数,直到重试结束,然后触发 RefreshAPI呼叫.
LogCat(不确定如何让它看起来漂亮):
05-05 16:12:07.145: E/Volley(1972): [137] BasicNetwork.performRequest:
Unexpected response code **401 for https://url.me/api/get_friends**
05-05 16:12:07.145: V/TokenRetryPolicy(1972): Initiating a retry
05-05 16:12:07.145: V/TokenRetryPolicy(1972): AuthFailureError found!
05-05 16:12:07.146: V/VolleyUser(1972): refresh token task called
05-05 16:12:07.146: V/RefreshAPICall(1972): RefreshAPICall made with username user_password
05-05 16:12:07.147: V/RefreshAPICall(1972): Priority set on refresh call is HIGH
05-05 16:12:07.147: V/RefreshAPICall(1972): Tag for Call is retry
05-05 16:12:07.265: E/Volley(1972): [137] BasicNetwork.performRequest: Unexpected response code **401 for https://url.me/api/get_friends**
05-05 16:12:07.265: V/TokenRetryPolicy(1972): Initiating a retry
05-05 16:12:07.265: V/TokenRetryPolicy(1972): AuthFailureError found!
05-05 16:12:07.265: V/VolleyUser(1972): refresh token task called
05-05 16:12:07.265: V/RefreshAPICall(1972): RefreshAPICall made with user user_password
05-05 16:12:07.265: V/RefreshAPICall(1972): Priority set on refresh call is HIGH
05-05 16:12:07.265: V/RefreshAPICall(1972): Tag for Call is retry
05-05 16:12:07.265: V/TokenRetryPolicy(1972): No attempt remaining, ERROR
05-05 16:12:08.219: I/Choreographer(1972): Skipped 324 frames! The application may be doing too much work on its main thread.
05-05 16:12:08.230: V/RefreshAPICall(1972): Response from server on refresh is: {"status":"success","token":"d5792e18c0e1acb3ad507dbae854eb2cdc5962a2c1b610a6b77e3bc3033c7f64"}
05-05 16:12:08.230: V/VolleyUser(1972): Token from return is: d5792e18c0e1acb3ad507dbae854eb2cdc5962a2c1b610a6b77e3bc3033c7f64
05-05 16:12:08.231: V/TokenRetryPolicy(1972): Cancelled all retry calls
05-05 16:12:08.257: V/SessionManager(1972): New Token In SharedPref is: d5792e18c0e1acb3ad507dbae854eb2cdc5962a2c1b610a6b77e3bc3033c7f64
05-05 16:12:08.257: V/TokenRetryPolicy(1972): Logged new token
现在发布一个答案,因为我找到了一种在重试时处理令牌刷新的半正经方法。
当我使用 Volley 创建我的一般(最常见)API 调用时,我会保存对调用的引用以防它失败,并将其传递给我的重试策略。
public GeneralAPICall(int method, String url, Map<String, String> params, Context context, Response.Listener<JSONObject> responseListener, Response.ErrorListener errorListener) {
super(method, url, errorListener);
sessionManager = SessionManager.getmInstance(context);
HashMap<String, String> credentials = sessionManager.getUserDetails(); // Get the user's credentials for authentication
this.listener = responseListener;
this.params = params;
// Encode the user's username and token
String loginEncoded = new String(Base64.encode((credentials.get(Constants.SessionManagerConstants.KEY_USERNAME)
+ Constants.APIConstants.Characters.CHAR_COLON
+ credentials.get(Constants.SessionManagerConstants.KEY_TOKEN)).getBytes(), Base64.NO_WRAP));
this.headers.put(Constants.APIConstants.BasicAuth.AUTHORIZATION, Constants.APIConstants.BasicAuth.BASIC + loginEncoded); // Set the encoded information as the header
setRetryPolicy(new TokenRetryPolicy(context, this)); //passing "this" saves the reference
}
然后,在我的重试策略 class 中(它只是扩展了 DefaultRetryPolicy,当我收到 401 错误告诉我需要一个新令牌时,我发出 refreshToken 调用以获取一个新令牌。
public class TokenRetryPolicy extends DefaultRetryPolicy implements IRefreshTokenReturn{
...
@Override
public void retry(VolleyError error) throws VolleyError {
mCurrentRetryCount++; //increment our retry count
mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
if (error instanceof AuthFailureError && sessionManager.isLoggedIn()) {
mCurrentRetryCount = mMaxNumRetries + 1; // Don't retry anymore, it's pointless
VolleyUser.refreshTokenTask(context, this); // Get new token
} if (!hasAttemptRemaining()) {
Log.v(LOG_TAG, "No attempt remaining, ERROR");
throw error;
}
}
...
}
调用 returns 后,我会在重试策略 class 中处理响应。我修改了失败的调用,给它新的令牌(在将令牌存储在 SharedPrefs 之后)来验证自己,然后再次启动它!
@Override
public void onTokenRefreshComplete(String token, String expiration) {
sessionManager.setStoredToken(token, expiration);
HashMap<String, String> credentials = sessionManager.getUserDetails(); //get the user's credentials for authentication
//encode the user's username and token
String loginEncoded = new String(Base64.encode((credentials.get(Constants.SessionManagerConstants.KEY_USERNAME)
+ Constants.APIConstants.Characters.CHAR_COLON
+ credentials.get(Constants.SessionManagerConstants.KEY_TOKEN)).getBytes(), Base64.NO_WRAP));
Log.v(LOG_TAG, loginEncoded); //TODO: remove
callThatFailed.setHeaders(Constants.APIConstants.BasicAuth.AUTHORIZATION, Constants.APIConstants.BasicAuth.BASIC + loginEncoded); //modify "old, failed" call - set the encoded information as the header
VolleySingleton.getInstance(context).getRequestQueue().add(callThatFailed);
Log.v(LOG_TAG, "fired off new call");
}
这个实现非常适合我。
但是,我应该注意这种情况应该不会发生太多,因为我了解到我应该在[=29=之前检查我的令牌是否已过期] 进行任何 API 调用。这可以通过在 SharedPrefs 中存储过期时间(从服务器返回)并查看是否 current_time - 过期时间 < some_time 来实现,其中 some_time 是您想要的时间量在它过期之前获得一个新令牌,对我来说是 10 秒。
希望这对那里的人有所帮助,如果我有任何错误,请发表评论!
我知道这个 post 这么老了,但是 post 在其他建议的解决方案之后我的解决方案对我没有帮助。
注意 - 我确实尝试了上面给出的 Brandon 的方法,即扩展 DefaultRetryPolicy。但它的字段是私有的,所以不想实现整个 class,必须有更好的方法。
所以我把代码写在CustomRequest class extending Request.以下是相关片段 -
在登录响应中存储令牌 -
@Override
protected Response<T> parseNetworkResponse(NetworkResponse response) {
...
//if oauth data is sent with response, store in SharedPrefs
...
}
如果访问令牌已过期 -
@Override
protected VolleyError parseNetworkError(VolleyError volleyError) {
...
if (volleyError instanceof NoConnectionError) {
//i know, there has to be a better way than checking this.
//will work on it later
if(volleyError.getMessage().equalsIgnoreCase("java.io.IOException: No authentication challenges found")) {
String accessToken = getNewAccessToken();//synchronous call
//retry
if(accessToken != null) {
//IMP: this is the statement which will retry the request manually
NetworkHelper.get(mContext).getRequestQueue().add(this);
}
}
}
...
}
将访问令牌附加到请求 -
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
...
String accesssToken = //get from SharedPrefs
headers.put("Authorization", "Bearer " +accessToken);
...
}
如果刷新令牌无效,将转到登录屏幕 -
private void showLogin(){
//stop all current requests
//cancelAllRequests();
Intent intent = new Intent(mContext, LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
mContext.startActivity(intent);
}
正在使用刷新令牌获取新的访问令牌。这必须是使用 RequestFuture -
的同步方法private String getNewAccessToken(){
...
//get new access token from server and store in SharedPrefs
...
//also return the new token so that we know if we need to retry or not
return newAccessToken;
}
HTH
我现在使用的策略是在失败重试的时候加上一个refreshToken。这是自定义失败重试。
public class CustomRetryPolicy implements RetryPolicy
{
private static final String TAG = "Refresh";
private Request request;
/**
* The current timeout in milliseconds.
*/
private int mCurrentTimeoutMs;
/**
* The current retry count.
*/
private int mCurrentRetryCount;
/**
* The maximum number of attempts.
*/
private final int mMaxNumRetries;
/**
* The backoff multiplier for the policy.
*/
private final float mBackoffMultiplier;
/**
* The default socket timeout in milliseconds
*/
public static final int DEFAULT_TIMEOUT_MS = 2500;
/**
* The default number of retries
*/
public static final int DEFAULT_MAX_RETRIES = 1;
/**
* The default backoff multiplier
*/
public static final float DEFAULT_BACKOFF_MULT = 1f;
/**
* Constructs a new retry policy using the default timeouts.
*/
public CustomRetryPolicy() {
this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
}
/**
* Constructs a new retry policy.
*
* @param initialTimeoutMs The initial timeout for the policy.
* @param maxNumRetries The maximum number of retries.
* @param backoffMultiplier Backoff multiplier for the policy.
*/
public CustomRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) {
mCurrentTimeoutMs = initialTimeoutMs;
mMaxNumRetries = maxNumRetries;
mBackoffMultiplier = backoffMultiplier;
}
/**
* Returns the current timeout.
*/
@Override
public int getCurrentTimeout() {
return mCurrentTimeoutMs;
}
/**
* Returns the current retry count.
*/
@Override
public int getCurrentRetryCount() {
return mCurrentRetryCount;
}
/**
* Returns the backoff multiplier for the policy.
*/
public float getBackoffMultiplier() {
return mBackoffMultiplier;
}
/**
* Prepares for the next retry by applying a backoff to the timeout.
*
* @param error The error code of the last attempt.
*/
@SuppressWarnings("unchecked")
@Override
public void retry(VolleyError error) throws VolleyError {
mCurrentRetryCount++;
mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
if (!hasAttemptRemaining()) {
throw error;
}
//401 and 403
if (error instanceof AuthFailureError) {//Just token invalid,refresh token
AuthFailureError er = (AuthFailureError) error;
if (er.networkResponse != null && er.networkResponse.statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
//Count is used to reset the flag
RefreshTokenManager instance = RefreshTokenManager.getInstance();
instance.increaseCount();
CUtils.logD(TAG, "come retry count: " + instance.getCount());
boolean ok = instance.refreshToken();
if (ok) {
Map<String, String> headers = request.getHeaders();
String[] tokens = instance.getTokens();
headers.put("token", tokens[0]);
Log.d(TAG, "retry:success");
} else {
throw error;
}
}
}
}
/**
* Returns true if this policy has attempts remaining, false otherwise.
*/
protected boolean hasAttemptRemaining() {
return mCurrentRetryCount <= mMaxNumRetries;
}
public Request getRequest() {
return request;
}
public void setRequest(Request request) {
this.request = request;
}
}
刷新令牌
public class RefreshTokenManager {
private static final String TAG = "Refresh";
private static RefreshTokenManager instance;
private final RefreshFlag flag;
/**
*retry count
*/
private AtomicInteger count = new AtomicInteger();
public int getCount() {
return count.get();
}
public int increaseCount() {
return count.getAndIncrement();
}
public void resetCount() {
this.count.set(0);
}
/**
* 锁
*/
private Lock lock;
public static RefreshTokenManager getInstance() {
synchronized (RefreshTokenManager.class) {
if (instance == null) {
synchronized (RefreshTokenManager.class) {
instance = new RefreshTokenManager();
}
}
}
return instance;
}
private RefreshTokenManager() {
flag = new RefreshFlag();
lock = new ReentrantLock();
}
public void resetFlag() {
lock.lock();
RefreshFlag flag = getFlag();
flag.resetFlag();
lock.unlock();
}
protected boolean refreshToken() {
lock.lock();
RefreshFlag flag = getFlag();
//Reset the flag so that the next time the token fails, it can enter normally.
if (flag.isFailure()) {
if (count.decrementAndGet() == 0) {
resetFlag();
}
lock.unlock();
return false;
} else if (flag.isSuccess()) {
CUtils.logD(TAG, "decrease retry count: " + instance.getCount());
if (count.decrementAndGet() == 0) {
count.incrementAndGet();
flag.resetFlag();
} else {
lock.unlock();
return true;
}
}
// refreshToken is doing.
flag.setDoing();
//Upload refresh_token and get the response from the server
String response = postRefreshTokenRequest();
CUtils.logD(TAG, "refreshToken: response " + response);
if (!TextUtils.isEmpty(response)) {
try {
JSONObject jsonObject = new JSONObject(response);
JSONObject data = jsonObject.optJSONObject("data");
if (data != null) {
String token = data.optString("token");
String refreshToken = data.optString("refresh_token");
CUtils.logD(TAG, "refreshToken: token : " + token + "\n" + "refresh_token : " + refreshToken);
if (!TextUtils.isEmpty(token) && !TextUtils.isEmpty(refreshToken)) {
//success,save token and refresh_token
saveTokens(token, refreshToken);
CUtils.logD(TAG, "run: success notify ");
flag.setSuccess();
if (count.decrementAndGet() == 0) {
resetFlag();
}
CUtils.logD(TAG, "decrease retry count: " + instance.getCount());
lock.unlock();
return true;
}
}
} catch (Exception e) {
CUtils.logE(e);
}
}
//delete local token and refresh_token
removeTokens();
flag.setFailure();
count.decrementAndGet();
CUtils.logD(TAG, "decrease retry count: " + instance.getCount());
lock.unlock();
CUtils.logD(TAG, "run: fail notify ");
return false;
}
private RefreshFlag getFlag() {
return flag;
}
}
这是旗帜
public final class RefreshFlag {
private static final int FLAG_SUCCESS = 0x01;
private static final int FLAG_DOING = 0x11;
private static final int FLAG_FAILURE = 0x10;
private static final int FLAG_INIT = 0x00;
/**
* flag 标志位
*/
private int flag = FLAG_INIT;
public boolean isDoingLocked() {
return flag == FLAG_DOING;
}
public void setDoing() {
flag = FLAG_DOING;
}
public void setSuccess() {
flag = FLAG_SUCCESS;
}
public void setFailure() {
flag = FLAG_FAILURE;
}
public boolean isSuccess() {
return flag == FLAG_SUCCESS;
}
public boolean isFailure() {
return flag == FLAG_FAILURE;
}
public void resetFlag() {
flag = FLAG_INIT;
}
}