Okhttp refresh expired token when multiple requests are sent to the server

前端 未结 4 1278
北荒
北荒 2021-01-30 13:14

I have a ViewPager and three webservice calls are made when ViewPager is loaded simultaneously.

When first one returns 401, Authenticato

相关标签:
4条回答
  • 2021-01-30 14:00

    You can try with this application level interceptor

     private class HttpInterceptor implements Interceptor {
    
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
    
            //Build new request
            Request.Builder builder = request.newBuilder();
            builder.header("Accept", "application/json"); //if necessary, say to consume JSON
    
            String token = settings.getAccessToken(); //save token of this request for future
            setAuthHeader(builder, token); //write current token to request
    
            request = builder.build(); //overwrite old request
            Response response = chain.proceed(request); //perform request, here original request will be executed
    
            if (response.code() == 401) { //if unauthorized
                synchronized (httpClient) { //perform all 401 in sync blocks, to avoid multiply token updates
                    String currentToken = settings.getAccessToken(); //get currently stored token
    
                    if(currentToken != null && currentToken.equals(token)) { //compare current token with token that was stored before, if it was not updated - do update
    
                        int code = refreshToken() / 100; //refresh token
                        if(code != 2) { //if refresh token failed for some reason
                            if(code == 4) //only if response is 400, 500 might mean that token was not updated
                                logout(); //go to login screen
                            return response; //if token refresh failed - show error to user
                        }
                    }
    
                    if(settings.getAccessToken() != null) { //retry requires new auth token,
                        setAuthHeader(builder, settings.getAccessToken()); //set auth token to updated
                        request = builder.build();
                        return chain.proceed(request); //repeat request with new token
                    }
                }
            }
    
            return response;
        }
    
        private void setAuthHeader(Request.Builder builder, String token) {
            if (token != null) //Add Auth token to each request if authorized
                builder.header("Authorization", String.format("Bearer %s", token));
        }
    
        private int refreshToken() {
            //Refresh token, synchronously, save it, and return result code
            //you might use retrofit here
        }
    
        private int logout() {
            //logout your user
        }
    }
    

    You can set interceptor like this to okHttp instance

        Gson gson = new GsonBuilder().create();
    
        OkHttpClient httpClient = new OkHttpClient();
        httpClient.interceptors().add(new HttpInterceptor());
    
        final RestAdapter restAdapter = new RestAdapter.Builder()
                .setEndpoint(BuildConfig.REST_SERVICE_URL)
                .setClient(new OkClient(httpClient))
                .setConverter(new GsonConverter(gson))
                .setLogLevel(RestAdapter.LogLevel.BASIC)
                .build();
    
        remoteService = restAdapter.create(RemoteService.class);
    

    Hope this helps!!!!

    0 讨论(0)
  • 2021-01-30 14:16

    It is important to note, that accountManager.blockingGetAuthToken (or the non-blocking version) could still be called somewhere else, other than the interceptor. Hence the correct place to prevent this issue from happening would be within the authenticator.

    We want to make sure that the first thread that needs an access token will retrieve it, and possible other threads should just register for a callback to be invoked when the first thread finished retrieving the token.
    The good news is, that AbstractAccountAuthenticator already has a way of delivering asynchronous results, namely AccountAuthenticatorResponse, on which you can call onResult or onError.


    The following sample consists of 3 blocks.

    The first one is about making sure that only one thread fetches the access token while other threads just register their response for a callback.

    The second part is just a dummy empty result bundle. Here, you would load your token, possibly refresh it, etc.

    The third part is what you do once you have your result (or error). You have to make sure to call the response for every other thread that might have registered.

    boolean fetchingToken;
    List<AccountAuthenticatorResponse> queue = null;
    
    @Override
    public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
    
      synchronized (this) {
        if (fetchingToken) {
          // another thread is already working on it, register for callback
          List<AccountAuthenticatorResponse> q = queue;
          if (q == null) {
            q = new ArrayList<>();
            queue = q;
          }
          q.add(response);
          // we return null, the result will be sent with the `response`
          return null;
        }
        // we have to fetch the token, and return the result other threads
        fetchingToken = true;
      }
    
      // load access token, refresh with refresh token, whatever
      // ... todo ...
      Bundle result = Bundle.EMPTY;
    
      // loop to make sure we don't drop any responses
      for ( ; ; ) {
        List<AccountAuthenticatorResponse> q;
        synchronized (this) {
          // get list with responses waiting for result
          q = queue;
          if (q == null) {
            fetchingToken = false;
            // we're done, nobody is waiting for a response, return
            return null;
          }
          queue = null;
        }
    
        // inform other threads about the result
        for (AccountAuthenticatorResponse r : q) {
          r.onResult(result); // return result
        }
    
        // repeat for the case another thread registered for callback
        // while we were busy calling others
      }
    }
    

    Just make sure to return null on all paths when using the response.

    You could obviously use other means to synchronize those code blocks, like atomics as shown by @matrix in another response. I made use of synchronized, because I believe this to be the easiest to grasp implementation, since this is a great question and everyone should be doing this ;)


    The above sample is an adapted version of an emitter loop described here, where it goes into great detail about concurrency. This blog is a great source if you're interested in how RxJava works under the hood.

    0 讨论(0)
  • 2021-01-30 14:17

    You can do this:

    Add those as data members:

    // these two static variables serve for the pattern to refresh a token
    private final static ConditionVariable LOCK = new ConditionVariable(true);
    private static final AtomicBoolean mIsRefreshing = new AtomicBoolean(false);
    

    and then on the intercept method:

    @Override
        public Response intercept(@NonNull Chain chain) throws IOException {
            Request request = chain.request();
    
            // 1. sign this request
            ....
    
            // 2. proceed with the request
            Response response = chain.proceed(request);
    
            // 3. check the response: have we got a 401?
            if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
    
                if (!TextUtils.isEmpty(token)) {
                    /*
                    *  Because we send out multiple HTTP requests in parallel, they might all list a 401 at the same time.
                    *  Only one of them should refresh the token, because otherwise we'd refresh the same token multiple times
                    *  and that is bad. Therefore we have these two static objects, a ConditionVariable and a boolean. The
                    *  first thread that gets here closes the ConditionVariable and changes the boolean flag.
                    */
                    if (mIsRefreshing.compareAndSet(false, true)) {
                        LOCK.close();
    
                        /* we're the first here. let's refresh this token.
                        *  it looks like our token isn't valid anymore.
                        *  REFRESH the actual token here
                        */
    
                        LOCK.open();
                        mIsRefreshing.set(false);
                    } else {
                        // Another thread is refreshing the token for us, let's wait for it.
                        boolean conditionOpened = LOCK.block(REFRESH_WAIT_TIMEOUT);
    
                        // If the next check is false, it means that the timeout expired, that is - the refresh
                        // stuff has failed.
                        if (conditionOpened) {
    
                            // another thread has refreshed this for us! thanks!
                            // sign the request with the new token and proceed
                            // return the outcome of the newly signed request
                            response = chain.proceed(newRequest);
                        }
                    }
                }
            }
    
            // check if still unauthorized (i.e. refresh failed)
            if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
                ... // clean your access token and prompt for request again.
            }
    
            // returning the response to the original request
            return response;
        }
    

    In this way you will only send 1 request to refresh the token and then for every other you will have the refreshed token.

    0 讨论(0)
  • 2021-01-30 14:18

    I found the solution with authenticator, the id is the number of the request, only for identification. Comments are in Spanish

     private final static Lock locks = new ReentrantLock();
    
    httpClient.authenticator(new Authenticator() {
                @Override
                public Request authenticate(@NonNull Route route,@NonNull Response response) throws IOException {
    
                    Log.e("Error" , "Se encontro un 401 no autorizado y soy el numero : " + id);
    
                    //Obteniendo token de DB
                    SharedPreferences prefs = mContext.getSharedPreferences(
                            BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
    
                    String token_db = prefs.getString("refresh_token","");
    
                    //Comparando tokens
                    if(mToken.getRefreshToken().equals(token_db)){
    
                        locks.lock(); 
    
                        try{
                            //Obteniendo token de DB
                             prefs = mContext.getSharedPreferences(
                                    BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
    
                            String token_db2 = prefs.getString("refresh_token","");
                            //Comparando tokens
                            if(mToken.getRefreshToken().equals(token_db2)){
    
                                //Refresh token
                                APIClient tokenClient = createService(APIClient.class);
                                Call<AccessToken> call = tokenClient.getRefreshAccessToken(API_OAUTH_CLIENTID,API_OAUTH_CLIENTSECRET, "refresh_token", mToken.getRefreshToken());
                                retrofit2.Response<AccessToken> res = call.execute();
                                AccessToken newToken = res.body();
                                // do we have an access token to refresh?
                                if(newToken!=null && res.isSuccessful()){
                                    String refreshToken = newToken.getRefreshToken();
    
                                        Log.e("Entra", "Token actualizado y soy el numero :  " + id + " : " + refreshToken);
    
                                        prefs = mContext.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
                                        prefs.edit().putBoolean("log_in", true).apply();
                                        prefs.edit().putString("access_token", newToken.getAccessToken()).apply();
                                        prefs.edit().putString("refresh_token", refreshToken).apply();
                                        prefs.edit().putString("token_type", newToken.getTokenType()).apply();
    
                                        locks.unlock();
    
                                        return response.request().newBuilder()
                                                .header("Authorization", newToken.getTokenType() + " " + newToken.getAccessToken())
                                                .build();
    
                                 }else{
                                    //Dirigir a login
                                    Log.e("redirigir", "DIRIGIENDO LOGOUT");
    
                                    locks.unlock();
                                    return null;
                                }
    
                            }else{
                                //Ya se actualizo tokens
    
                                Log.e("Entra", "El token se actualizo anteriormente, y soy el no : " + id );
    
                                prefs = mContext.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
    
                                String type = prefs.getString("token_type","");
                                String access = prefs.getString("access_token","");
    
                                locks.unlock();
    
                                return response.request().newBuilder()
                                        .header("Authorization", type + " " + access)
                                        .build();
                            }
    
                        }catch (Exception e){
                            locks.unlock();
                            e.printStackTrace();
                            return null;
                        }
    
    
                    }
                    return null;
                }
            });
    
    0 讨论(0)
提交回复
热议问题