在使用Volley

时间:2015-05-05 20:19:42

标签: authentication queue token android-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;
    }
}

刷新令牌任务定义了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

3 个答案:

答案 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