使用Angular拦截Unathorized API调用

时间:2015-05-04 15:25:15

标签: javascript angularjs interceptor angular-promise

我试图拦截401403错误来刷新用户令牌,但我无法让它运行良好。我所取得的就是这个拦截器:

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会进行无限循环的刷新'因为具有更新令牌的授权标头,$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)点,因为Authorization标头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&amp;后端试图重现这个问题。

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

1 个答案:

答案 0 :(得分:9)

您的拦截器需要跟踪它是否有“在飞行中”的新身份验证令牌请求。如果是这样,您需要等待正在进行的请求的结果,而不是启动新的请求。您可以通过缓存promise返回的AuthRequest并使用缓存的承诺而不是为每个API请求创建新的承诺来执行此操作。

Here is an answer to a similar question that demonstrates this

对于您的示例 - 这是一个示例实现:

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);
      }
    }
  });
});

更新:

我不清楚你的问题究竟是什么问题,但你的AuthenticationService存在问题。建议的更改低于and here is a Plunkr,但更为完整(包括跟踪机上信息请求):

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

  //this deferred declaration should be moved.  As it is, it's 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;
    }
  };
});

作为最后一点,跟踪机上getToken请求和inflight refreshToken请求将阻止您对服务器进行过多调用。在高负荷下,您可能会创建比您需要更多的访问权限。

更新2:

此外,查看代码时,如果出现401错误,则调用refreshToken()。但是,refreshToken不会将新令牌信息放入会话高速缓存中,因此新请求将继续使用旧令牌。更新了Plunkr。