我有一个使用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;
}
}
刷新令牌任务定义了RefreshAPICall
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);
}
我们的RefreshAPICall定义:
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优先级对队列调度呼叫的顺序没有影响。
有关如何确保我的RefreshAPICall被激活的任何帮助都会被激发,然后才会非常感谢其他任务。我想知道Volley是否将RefreshAPICall视为原始失败任务的子任务,因此它会尝试将该原始任务调用为其重试次数,直到这些任务结束,然后触发RefreshAPICall。
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
答案 0 :(得分:12)
现在我发现了一个答案,我找到了一种在重试时处理令牌刷新的方法。
当我使用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
}
然后,在我的重试策略类中(它只是扩展了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;
}
}
...
}
一旦该调用返回,我将在我的重试策略类中处理响应。我修改失败的调用,给它新的令牌(在将标记存储在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");
}
这个实现对我很有用。
但是,我应该注意,这种情况不应该发生太多,因为我了解到我应该检查我的令牌是否已经过期 进行任何API调用之前。这可以通过在SharedPrefs中存储到期时间(从服务器返回),并查看current_time - 到期时间&lt; some_time,some_time是你想要在它到期之前得到一个新令牌的时间,对我来说是10秒。
希望这可以帮助那里的人,如果我对任何事情有误,请发表评论!
答案 1 :(得分:1)
我现在使用的策略是将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;
}
}
RefreshToken
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;
}
}
答案 2 :(得分:0)
我知道这篇文章很老了,但在其他解决方案建议之后发布我的解决方案并没有帮助我。
注意 - 我确实尝试过上面给出的Brandon方法,即扩展DefaultRetryPolicy。但它的字段是私有的,所以不想实现全班,必须有更好的方法。
所以我在扩展Request的CustomRequest类中编写代码。以下是相关摘要 -
在登录回复中存储令牌 -
@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