【问题标题】:Intercept Unathorized API calls with Angular使用 Angular 拦截未经授权的 API 调用
【发布时间】:2015-07-14 01:24:28
【问题描述】:

我正在尝试拦截 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) 点,因为授权标头从来没有新的刷新的有效令牌,而是有过期的令牌。我不知道为什么,因为它应该设置在responseError

身份验证工厂

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 & backend 来尝试重现这个问题。

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

【问题讨论】:

  • 你最后的陈述是什么意思?您的意思是在收到刷新令牌后,对$http 的下一次调用再次导致 403 或 401?而且,顺便说一句,AuthenticationFactory 对我们来说存在巨大的信息差距——即使从概念上讲,刷新和令牌案例之间的情况也不清楚。刷新调用是否会更新令牌,以便下次.getToken 返回新令牌?
  • 最后一条语句意味着应用程序正在使用 PREVIOUS(过期)令牌进行 API 调用,而不是我猜到的更新的令牌,因为我使用 response.config.headers.Authorization = token; 设置它。是的,刷新更新并为以下.getToken 调用设置令牌
  • 哪个 API 调用?是$http(response.config)还是刷新令牌的那个?
  • 那个$http(response.config)
  • $http.defaults.headers.common.Authorization 将为每个 $http 调用设置标头,而不仅仅是 API 调用。这很不安全……例如中间人

标签: javascript angularjs interceptor angular-promise


【解决方案1】:

您的拦截器需要跟踪它是否有“正在运行”的新身份验证令牌请求。如果是这样,您需要等待进行中请求的结果,而不是发起新的请求。您可以通过缓存 AuthRequest 返回的 promise 并使用缓存的 promise 而不是为每个 API 请求创建新的 promise 来做到这一点。

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 请求和飞行中的 refreshToken 请求可以防止您对服务器进行过多调用。在高负载下,您可能会创建比您需要的更多的访问令牌。

更新 2:

此外,查看代码,当您收到 401 错误时,您正在调用 refreshToken()。但是,refreshToken 不会将新令牌信息放入会话缓存中,因此新请求将继续使用旧令牌。更新了 Plunkr。

【讨论】:

  • 如果您不介意我的提问,您的服务器逻辑是否会在发行新令牌时使现有令牌无效?
  • 令牌因过期时间失效,因此服务器以401403 状态码响应,客户端在responseError 捕获它以将令牌替换为refresh_token
  • 查看了您的 Plunkr。我不清楚它是否会重现您的问题。但是,我确实发现了一个问题,涉及您的身份验证工厂在应该创建新的延迟时重用延迟。在已经解决的承诺上调用 resolve 将是一个 noop。我已经发布了建议的更改。在不知道您的确切问题的情况下,我只能推测它会解决您的问题,但我怀疑它会。
  • @JoeEnzminger,我只想摆脱所有这些deferred 并链接实际的$http 生成的承诺。并且还要使 refreshToken API 成为内部的,因为 getToken 无论如何都会回退到它
  • 好吧,乔,成功了!问题是重用延迟。现在它就像一个魅力。非常感谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-27
  • 1970-01-01
  • 2011-11-18
  • 2017-04-22
  • 2018-07-08
  • 2018-06-26
相关资源
最近更新 更多