为WebTokens身份验证改进自定义客户端

时间:2015-10-03 18:46:10

标签: android multithreading retrofit okhttp

我正在使用Retrofit处理与服务器API的通信,API用户JSON Web Tokens用于身份验证。令牌不时到期,我正在寻找实现Retrofit Client的最佳方法,该客户端可以在到期时自动刷新令牌。

这是我提出的初步实施,:

/**
* Client implementation that refreshes JSON WebToken automatically if
* the response contains a 401 header, has there may be simultaneous calls to execute method
* the refreshToken is synchronized to avoid multiple login calls.
*/
public class RefreshTokenClient extends OkClient {


private static final int UNAUTHENTICATED = 401;


/**
 * Application context
 */
private Application mContext;



public RefreshTokenClient(OkHttpClient client, Application application) {
    super(client);
    mContext = application;
}


@Override
public Response execute(Request request) throws IOException {

    Timber.d("Execute request: " + request.getMethod() + " - " + request.getUrl());

    //Make the request and check for 401 header
    Response response = super.execute( request );

    Timber.d("Headers: "+ request.getHeaders());

    //If we received a 401 header, and we have a token, it's most likely that
    //the token we have has expired
    if(response.getStatus() == UNAUTHENTICATED && hasToken()) {

        Timber.d("Received 401 from server awaiting");

        //Clear the token
        clearToken();

        //Gets a new token
        refreshToken(request);

        //Update token in the request
        Timber.d("Make the call again with the new token");

        //Makes the call again
        return super.execute(rebuildRequest(request));

    }

    return response;
}


/**
 * Rebuilds the request to be executed, overrides the headers with the new token
 * @param request
 * @return new request to be made
 */
private Request rebuildRequest(Request request){

    List<Header> newHeaders = new ArrayList<>();
    for( Header h : request.getHeaders() ){
        if(!h.getName().equals(Constants.Headers.USER_TOKEN)){
            newHeaders.add(h);
        }
    }
    newHeaders.add(new Header(Constants.Headers.USER_TOKEN,getToken()));
    newHeaders = Collections.unmodifiableList(newHeaders);

    Request r = new Request(
            request.getMethod(),
            request.getUrl(),
            newHeaders,
            request.getBody()
    );

    Timber.d("Request url: "+r.getUrl());
    Timber.d("Request new headers: "+r.getHeaders());

    return r;
}

/**
 * Do we have a token
 */
private boolean hasToken(){
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
    return prefs.contains(Constants.TOKEN);
}

/**
 * Clear token
 */
private void clearToken(){
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
    prefs.edit().remove(Constants.TOKEN).commit();
}

/**
 * Saves token is prefs
 */
private void saveToken(String token){
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
    prefs.edit().putString(Constants.TOKEN, token).commit();
    Timber.d("Saved new token: " + token);
}

/**
 * Gets token
 */
private String getToken(){
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
    return prefs.getString(Constants.TOKEN,"");
}




/**
 * Refreshes the token by making login again,
 * //TODO implement refresh token endpoint, instead of making another login call
 */
private synchronized void refreshToken(Request oldRequest) throws IOException{

    //We already have a token, it means a refresh call has already been made, get out
    if(hasToken()) return;

    Timber.d("We are going to refresh token");

    //Get credentials
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
    String email    = prefs.getString(Constants.EMAIL, "");
    String password = prefs.getString(Constants.PASSWORD, "");

    //Login again 
    com.app.bubbles.model.pojos.Response<Login> res = ((App) mContext).getApi().login(
            new com.app.bubbles.model.pojos.Request<>(credentials)
    );

    //Save token in prefs
    saveToken(res.data.getTokenContainer().getToken());

    Timber.d("Token refreshed");
}


}

我不深入了解Retrofit / OkHttpClient的体系结构,但据我所知,execute方法可以从多个线程多次调用,OkClient仅在Calls之间共享一个浅的副本完成。 我在synchronized方法中使用refreshToken()来避免多个线程进入refreshToken()并进行多次登录调用,只需要刷新一个线程就应该刷新,其他人将使用更新的令牌。

我还没有认真测试过它,但是我能看到它的工作正常。也许有人已经有了这个问题并且可以分享他的解决方案,或者对于有相同/类似问题的人有帮助。

感谢。

1 个答案:

答案 0 :(得分:7)

对于任何发现此问题的人,您应该使用OkHttp拦截器或使用Authenticator API

这是来自Retrofit GitHub页面的示例

public void setup() {
    OkHttpClient client = new OkHttpClient();
    client.interceptors().add(new TokenInterceptor(tokenManager));

    Retrofit retrofit = new Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create())
            .client(client)
            .baseUrl("http://localhost")
            .build();
}

private static class TokenInterceptor implements Interceptor {
    private final TokenManager mTokenManager;

    private TokenInterceptor(TokenManager tokenManager) {
        mTokenManager = tokenManager;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request initialRequest = chain.request();
        Request modifiedRequest = request;
        if (mTokenManager.hasToken()) {
            modifiedRequest = request.newBuilder()
                    .addHeader("USER_TOKEN", mTokenManager.getToken())
                    .build();
        }

        Response response = chain.proceed(modifiedRequest);
        boolean unauthorized = response.code() == 401;
        if (unauthorized) {
            mTokenManager.clearToken();
            String newToken = mTokenManager.refreshToken();
            modifiedRequest = request.newBuilder()
                    .addHeader("USER_TOKEN", mTokenManager.getToken())
                    .build();
             return chain.proceed(modifiedRequest);
        }
        return response;
    }
}

interface TokenManager {
    String getToken();
    boolean hasToken();
    void clearToken();
    String refreshToken();
}

如果要在身份验证完成之前阻止请求,可以使用我在答案中执行的相同同步机制,因为拦截器可以在多个线程上并发运行