promesas javascript angularjs interceptor angular-promise

javascript - promesas - Interceptar llamadas API no autorizadas con Angular



promesas en angular 6 (1)

Estoy intentando interceptar los errores 401 y 403 para actualizar el token de usuario, pero no puedo hacer que funcione correctamente. Todo lo que he logrado es este interceptor:

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

El problema es que responseError hace un bucle infinito de ''renovaciones'' porque, por el encabezado de Autorización con el token actualizado, la llamada $http(response.config) no está recibiendo.

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)`

El bucle está ocurriendo en el punto (3.3) porque el encabezado de autorización NUNCA tiene el nuevo token válido actualizado, sino que tiene el token caducado. No sé por qué, ya que se supone que se establece en la 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

Hice un plunker y backend para tratar de reproducir este problema.

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


Su interceptor debe realizar un seguimiento de si tiene o no una solicitud de un nuevo token de autenticación "en vuelo". Si es así, debe esperar el resultado de la solicitud en vuelo en lugar de iniciar una nueva. Puede hacer esto almacenando en caché la promise devuelta por su AuthRequest y utilizando la promesa almacenada en caché en lugar de crear una nueva para cada solicitud de API.

Aquí hay una respuesta a una pregunta similar que demuestra esto .

Para su ejemplo, aquí hay una implementación de ejemplo:

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

ACTUALIZAR:

No queda claro para usted exactamente cuál es el problema, pero hay un problema con su Servicio de Autenticación. Los cambios recomendados se encuentran a continuación y aquí hay un Plunkr que es un poco más completo (e incluye solicitudes de seguimiento de vuelo):

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

Como nota final, realizar un seguimiento de las solicitudes getToken durante el vuelo y las solicitudes refreshToken durante el vuelo evitará que hagas demasiadas llamadas a tu servidor. Bajo una carga alta, puede que esté creando más tokens de acceso de los que necesita.

ACTUALIZACIÓN 2:

Además, al revisar el código, cuando recibe un error 401, está llamando a refreshToken (). Sin embargo, refreshToken no coloca la nueva información del token en el caché de la sesión, por lo que las nuevas solicitudes continuarán usando el token anterior. Actualizado el Plunkr.