Подтвердить что ты не робот

Перехват вызовов Unathorized API с помощью Angular

Я пытаюсь перехватить ошибки 401 и 403, чтобы обновить токен пользователя, но я не могу заставить его работать хорошо. Все, что я достиг, это перехватчик:

app.config(function ($httpProvider) {

  $httpProvider.interceptors.push(function ($q, $injector) {

    return {
      // On request success
      request: function (config) {
        var deferred = $q.defer();

        if ((config.url.indexOf('API URL') !== -1)) {
          // If any API resource call, get the token firstly
          $injector.get('AuthenticationFactory').getToken().then(function (token) {
            config.headers.Authorization = token;

            deferred.resolve(config);
          });
        } else {
          deferred.resolve(config);
        }

        return deferred.promise;
      },

      response: function (response) {
        // Return the promise response.
        return response || $q.when(response);
      },

      responseError: function (response) {
        // Access token invalid or expired
        if (response.status == 403 || response.status == 401) {
          var $http = $injector.get('$http');
          var deferred = $q.defer();

          // Refresh token!
          $injector.get('AuthenticationFactory').getToken().then(function (token) {
            response.config.headers.Authorization = token;

            $http(response.config).then(deferred.resolve, deferred.reject);
          });

          return deferred.promise;
        }

        return $q.reject(response);
      }
    }
  });
});

Проблема заключается в том, что responseError выполняет бесконечный цикл 'refreshes', потому что заголовок авторизации с обновленным токеном, который не принимается вызовом $http(response.config).

1.- App has an invalid token stored.
2.- App needs to do an API call
  2.1 Interceptor catch the `request`.
  2.2 Get the (invalid) stored token and set the Authorization header.
  2.3 Interceptor does the API call with the (invalid) token setted.
3.- API respond that used token is invalid or expired (403 or 401 statuses)
  3.1 Interceptor catch the `responseError`
  3.2 Refresh the expired token, get a new VALID token and set it in the Authorization header.
  3.3 Retry the point (2) with the valid refreshed token `$http(response.config)`

Цикл происходит в точке (3.3), потому что заголовок авторизации NEVER имеет новый обновленный действительный токен, вместо этого он имеет истекший токен. Я не знаю, почему, потому что он должен быть установлен в responseError

AuthenticationFactory

app.factory('AuthenticationFactory', function($rootScope, $q, $http, $location, $log, URI, SessionService) {

  var deferred = $q.defer();

  var cacheSession   = function(tokens) {
    SessionService.clear();

    // Then, we set the tokens
    $log.debug('Setting tokens...');
    SessionService.set('authenticated', true);
    SessionService.set('access_token', tokens.access_token);
    SessionService.set('token_type', tokens.token_type);
    SessionService.set('expires', tokens.expires);
    SessionService.set('expires_in', tokens.expires_in);
    SessionService.set('refresh_token', tokens.refresh_token);
    SessionService.set('user_id', tokens.user_id);

    return true;
  };

  var uncacheSession = function() {
    $log.debug('Logging out. Clearing all');
    SessionService.clear();
  };

  return {
    login: function(credentials) {
      var login = $http.post(URI+'/login', credentials).then(function(response) {
        cacheSession(response.data);
      }, function(response) {
        return response;
      });

      return login;
    },
    logout: function() {
      uncacheSession();
    },
    isLoggedIn: function() {
      if(SessionService.get('authenticated')) {
        return true;
      }
      else {
        return false;
      }
    },
    isExpired: function() {
      var unix = Math.round(+new Date()/1000);

      if (unix < SessionService.get('expires')) {
        // not expired
        return false;
      }

      // If not authenticated or expired
      return true;
    },
    refreshToken: function() {
      var request_params = {
        grant_type:     "refresh_token",
        refresh_token:  SessionService.get('refresh_token')
      };

      return $http({
          method: 'POST',
          url: URI+'/refresh',
          data: request_params
        });
    },
    getToken: function() {
      if( ! this.isExpired()) {
        deferred.resolve(SessionService.get('access_token'));
      } else {
        this.refreshToken().then(function(response) {
          $log.debug('Token refreshed!');

          if(angular.isUndefined(response.data) || angular.isUndefined(response.data.access_token))
          {
            $log.debug('Error while trying to refresh token!');
            uncacheSession();
          }
          else {

            SessionService.set('access_token', response.data.access_token);
            SessionService.set('token_type', response.data.token_type);
            SessionService.set('expires', tokens.expires);
            SessionService.set('expires_in', response.data.expires_in);

            deferred.resolve(response.data.access_token);
          }
        }, function() {
          // Error
          $log.debug('Error while trying to refresh token!');
          uncacheSession();
        });
      }

      return deferred.promise;
    }
  };
});

PLUNKER

Я сделал plunker и backend, чтобы попытаться воспроизвести эту проблему.

http://plnkr.co/edit/jaJBEohqIJayk4yVP2iN?p=preview

4b9b3361

Ответ 1

Ваш перехватчик должен отслеживать, есть ли у него запрос на новый токен аутентификации "в полете". Если это так, вам нужно ждать результата запроса в полете, а не инициировать новый. Вы можете сделать это, кэшируя promise, возвращенный вашим AuthRequest, и используя обещание с кешем вместо создания нового для каждого запроса API.

Вот ответ на аналогичный вопрос, демонстрирующий это.

Для примера - вот пример реализации:

app.config(function ($httpProvider) {

$httpProvider.interceptors.push(function ($q, $injector) {
    var inFlightRequest = null;
    return {
      // On request success
      request: function (config) {
        var deferred = $q.defer();

        if ((config.url.indexOf('API URL') !== -1)) {
          // If any API resource call, get the token firstly
          $injector.get('AuthenticationFactory').getToken().then(function (token) {
            config.headers.Authorization = token;

            deferred.resolve(config);
          });
        } else {
          deferred.resolve(config);
        }

        return deferred.promise;
      },

      response: function (response) {
        // Return the promise response.
        return response || $q.when(response);
      },

      responseError: function (response) {
        // Access token invalid or expired
        if (response.status == 403 || response.status == 401) {
          var $http = $injector.get('$http');
          var deferred = $q.defer();

          // Refresh token!
          if(!inFlightRequest){
             inFlightRequest = $injector.get('AuthenticationFactory').refreshToken();
          }
          //all requests will wait on the same auth request now:
          inFlightRequest.then(function (token) {
            //clear the inFlightRequest so that new errors will generate a new AuthRequest.
            inFlightRequest = null;
            response.config.headers.Authorization = token;

            $http(response.config).then(deferred.resolve, deferred.reject);
          }, function(err){
              //error handling omitted for brevity
          });

          return deferred.promise;
        }

        return $q.reject(response);
      }
    }
  });
});

UPDATE:

Мне непонятно, с какой именно проблемой, но проблема с вашим AuthenticationService. Рекомендуемые изменения ниже и вот Plunkr, который немного более полный (и включает в себя запросы отслеживания вторжения):

app.factory('AuthenticationFactory', function($rootScope, $q, $http, $location, $log, URI, SessionService) {

  //this deferred declaration should be moved.  As it is, it created once and re-resolved many times, which isn't how promises work.  Subsequent calls to resolve essentially are noops.  

  //var deferred = $q.defer();

  var cacheSession   = function(tokens) {
    SessionService.clear();

    // Then, we set the tokens
    $log.debug('Setting tokens...');
    SessionService.set('authenticated', true);
    SessionService.set('access_token', tokens.access_token);
    SessionService.set('token_type', tokens.token_type);
    SessionService.set('expires', tokens.expires);
    SessionService.set('expires_in', tokens.expires_in);
    SessionService.set('refresh_token', tokens.refresh_token);
    SessionService.set('user_id', tokens.user_id);

    return true;
  };

  var uncacheSession = function() {
    $log.debug('Logging out. Clearing all');
    SessionService.clear();
  };

  return {
    login: function(credentials) {
      var login = $http.post(URI+'/login', credentials).then(function(response) {
        cacheSession(response.data);
      }, function(response) {
        return response;
      });

      return login;
    },
    logout: function() {
      uncacheSession();
    },
    isLoggedIn: function() {
      if(SessionService.get('authenticated')) {
        return true;
      }
      else {
        return false;
      }
    },
    isExpired: function() {
      var unix = Math.round(+new Date()/1000);

      if (unix < SessionService.get('expires')) {
        // not expired
        return false;
      }

      // If not authenticated or expired
      return true;
    },
    refreshToken: function() {
      var request_params = {
        grant_type:     "refresh_token",
        refresh_token:  SessionService.get('refresh_token')
      };

      return $http({
          method: 'POST',
          url: URI+'/refresh',
          data: request_params
        });
    },
    getToken: function() {

      //It should be moved here - a new defer should be created for each invocation of getToken();
      var deferred = $q.defer();          

      if( ! this.isExpired()) {
        deferred.resolve(SessionService.get('access_token'));
      } else {
        this.refreshToken().then(function(response) {
          $log.debug('Token refreshed!');

          if(angular.isUndefined(response.data) || angular.isUndefined(response.data.access_token))
          {
            $log.debug('Error while trying to refresh token!');
            uncacheSession();
          }
          else {

            SessionService.set('access_token', response.data.access_token);
            SessionService.set('token_type', response.data.token_type);
            SessionService.set('expires', tokens.expires);
            SessionService.set('expires_in', response.data.expires_in);

            deferred.resolve(response.data.access_token);
          }
        }, function() {
          // Error
          $log.debug('Error while trying to refresh token!');
          uncacheSession();
        });
      }

      return deferred.promise;
    }
  };
});

В качестве окончательного примечания, отслеживание как запросов inflight getToken, так и запросов inflight refreshToken не позволит вам совершать слишком много вызовов на ваш сервер. При высокой нагрузке вы можете создать больше токенов доступа, чем вам нужно.

ОБНОВЛЕНИЕ 2:

Также, просматривая код, когда вы получаете ошибку 401, вы вызываете refreshToken(). Однако refreshToken не помещает новую информацию о токенах в кеш сеанса, поэтому новые запросы будут продолжать использовать старый токен. Обновлен Plunkr.