在使用 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;
}
}