android - 为 WebTokens 身份验证改造自定义客户端

标签 android multithreading retrofit okhttp

我正在使用 Retrofit 来处理与服务器 API 的通信,API 用户使用 JSON Web Tokens 进行身份验证。 token 有时会过期,我正在寻找实现可在 token 过期时自动刷新 token 的 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 之间共享相同调用 只完成一个浅拷贝。 我在 refreshToken() 方法中使用 synchronized 来避免多个线程进入 refreshToken() 并进行多次登录调用,我刷新是只需要一个线程进行 refreshCall,其他线程将使用更新的 token 。

我还没有认真测试过它,但据我所知它运行良好。也许有人已经遇到过这个问题并可以分享他的解决方案,或者对遇到相同/类似问题的人有帮助。

谢谢。

最佳答案

对于任何发现此问题的人,您应该使用 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();
}

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

关于android - 为 WebTokens 身份验证改造自定义客户端,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32926190/

相关文章:

java - 使 toast 在互联网连接等待循环之前显示

安卓模拟器 : Failed to allocate memory: 8 even with 8MB RAM

android - 当 JSON 数据是动态的时,如何在 Android 中进行改造?

java - 如何创建一个四位数的密码 Android 布局

android - 我应该使用什么权限来接收 android 通知?

java - 如何创建同步数组列表

java - ThreadPoolExecutor 的 corePoolSize=0 如何工作?

c++ - 是否可以创建一个线程组,然后只有 "use"线程?

android - 如何使用 Retrofit 2 从 JSON 列表中获取 List<String>?

android - 如何修复尝试通过改造抛出 OutOfMemoryError 时抛出的 OutOfMemoryError