AngularJS: inyección de servicio en un interceptor HTTP (dependencia circular)

118

Estoy tratando de escribir un interceptor HTTP para que mi aplicación AngularJS maneje la autenticación.

Este código funciona, pero me preocupa inyectar manualmente un servicio ya que pensé que se supone que Angular maneja esto automáticamente:

    app.config(['$httpProvider', function ($httpProvider) {
    $httpProvider.interceptors.push(function ($location, $injector) {
        return {
            'request': function (config) {
                //injected manually to get around circular dependency problem.
                var AuthService = $injector.get('AuthService');
                console.log(AuthService);
                console.log('in request interceptor');
                if (!AuthService.isAuthenticated() && $location.path != '/login') {
                    console.log('user is not logged in.');
                    $location.path('/login');
                }
                return config;
            }
        };
    })
}]);

Lo que comencé a hacer, pero encontré problemas de dependencia circular:

    app.config(function ($provide, $httpProvider) {
    $provide.factory('HttpInterceptor', function ($q, $location, AuthService) {
        return {
            'request': function (config) {
                console.log('in request interceptor.');
                if (!AuthService.isAuthenticated() && $location.path != '/login') {
                    console.log('user is not logged in.');
                    $location.path('/login');
                }
                return config;
            }
        };
    });

    $httpProvider.interceptors.push('HttpInterceptor');
});

Otra razón por la que me preocupa es que la sección sobre $ http en Angular Docs parece mostrar una forma de inyectar las dependencias de la "forma normal" en un interceptor Http. Consulte su fragmento de código en "Interceptores":

// register the interceptor as a service
$provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
  return {
    // optional method
    'request': function(config) {
      // do something on success
      return config || $q.when(config);
    },

    // optional method
   'requestError': function(rejection) {
      // do something on error
      if (canRecover(rejection)) {
        return responseOrNewPromise
      }
      return $q.reject(rejection);
    },



    // optional method
    'response': function(response) {
      // do something on success
      return response || $q.when(response);
    },

    // optional method
   'responseError': function(rejection) {
      // do something on error
      if (canRecover(rejection)) {
        return responseOrNewPromise
      }
      return $q.reject(rejection);
    };
  }
});

$httpProvider.interceptors.push('myHttpInterceptor');

¿Dónde debería ir el código anterior?

Supongo que mi pregunta es ¿cuál es la forma correcta de hacer esto?

Gracias, y espero que mi pregunta haya sido lo suficientemente clara.

Shaunlim
fuente
1
Solo por curiosidad, ¿qué dependencias (si las hay) utiliza en su AuthService? Tenía problemas de dependencia circular al usar el método de solicitud en el interceptor http, lo que me trajo aquí. Estoy usando $ firebaseAuth de angularfire. Cuando eliminé el bloque de código que usa $ route del inyector (línea 510), todo comenzó a funcionar. Hay un problema aquí, pero se trata de usar $ http en el interceptor. ¡Vamos a ir!
Slamborne
Hm, por lo que vale, AuthService en mi caso depende de $ window, $ http, $ location, $ q
shaunlim
Tengo un caso que vuelve a intentar la solicitud en el interceptor en algunas circunstancias, por lo que hay una dependencia circular aún más corta en $http. La única forma de evitarlo que he encontrado es usar $injector.get, pero sería genial saber si hay una buena manera de estructurar el código para evitar esto.
Michal Charemza
1
Eche un vistazo a la respuesta de @rewritten: github.com/angular/angular.js/issues/2367 que solucionó un problema similar para mí. Lo que está haciendo es así: $ http = $ http || $ injector.get ("$ http"); por supuesto, puede reemplazar $ http con su propio servicio que está intentando utilizar.
Jonathan

Respuestas:

42

Tiene una dependencia circular entre $ http y su AuthService.

Lo que está haciendo al usar el $injectorservicio es resolver el problema del huevo y la gallina al retrasar la dependencia de $ http en AuthService.

Creo que lo que hiciste es en realidad la forma más sencilla de hacerlo.

También puede hacer esto:

  • Registrar el interceptor más tarde (hacerlo en un run()bloque en lugar de un config()bloque podría ser suficiente). ¿Pero puede garantizar que no se haya llamado a $ http ya?
  • "Inyectando" $ http manualmente en AuthService cuando está registrando el interceptor llamando AuthService.setHttp()o algo.
  • ...
Pieter Herroelen
fuente
15
¿Cómo esta respuesta está resolviendo el problema, no vi? @shaunlim
Inanc Gumus
1
En realidad, no lo está resolviendo, solo señala que el flujo del algoritmo estaba mal.
Roman M. Koss
12
No puede registrar el interceptor en un run()bloque, porque no puede inyectar $ httpProvider en un bloque de ejecución. Solo puede hacer eso en la fase de configuración.
Stephen Friedrich
2
Buen punto de referencia circular, pero de lo contrario no debería ser una respuesta aceptada. Ninguno de los puntos tiene sentido
Nikolai
65

Esto es lo que terminé haciendo

  .config(['$httpProvider', function ($httpProvider) {
        //enable cors
        $httpProvider.defaults.useXDomain = true;

        $httpProvider.interceptors.push(['$location', '$injector', '$q', function ($location, $injector, $q) {
            return {
                'request': function (config) {

                    //injected manually to get around circular dependency problem.
                    var AuthService = $injector.get('Auth');

                    if (!AuthService.isAuthenticated()) {
                        $location.path('/login');
                    } else {
                        //add session_id as a bearer token in header of all outgoing HTTP requests.
                        var currentUser = AuthService.getCurrentUser();
                        if (currentUser !== null) {
                            var sessionId = AuthService.getCurrentUser().sessionId;
                            if (sessionId) {
                                config.headers.Authorization = 'Bearer ' + sessionId;
                            }
                        }
                    }

                    //add headers
                    return config;
                },
                'responseError': function (rejection) {
                    if (rejection.status === 401) {

                        //injected manually to get around circular dependency problem.
                        var AuthService = $injector.get('Auth');

                        //if server returns 401 despite user being authenticated on app side, it means session timed out on server
                        if (AuthService.isAuthenticated()) {
                            AuthService.appLogOut();
                        }
                        $location.path('/login');
                        return $q.reject(rejection);
                    }
                }
            };
        }]);
    }]);

Nota: Las $injector.getllamadas deben estar dentro de los métodos del interceptor, si intenta usarlas en otro lugar, continuará obteniendo un error de dependencia circular en JS.

Shaunlim
fuente
4
El uso de la inyección manual ($ injector.get ('Auth')) resolvió el problema. ¡Buen trabajo!
Robert
Para evitar la dependencia circular, estoy comprobando qué URL se llama. if (! config.url.includes ('/ oauth / v2 / token') && config.url.includes ('/ api')) {// Llamar al servicio OAuth}. Por lo tanto, no hay más dependencia circular. Al menos para mí funcionó;).
Brieuc
Perfecto. Esto es exactamente lo que necesitaba para solucionar un problema similar. ¡Gracias @shaunlim!
Martyn Chamberlin
Realmente no me gusta esta solución porque entonces este servicio es anónimo y no es tan fácil de manejar para las pruebas. Mucho mejor solución para inyectar en tiempo de ejecución.
kmanzana
Eso funcionó para mí. Básicamente inyectando el servicio que estaba usando $ http.
Thomas
15

Creo que usar el inyector $ directamente es un antipatrón.

Una forma de romper la dependencia circular es usar un evento: en lugar de inyectar $ state, inyecte $ rootScope. En lugar de redirigir directamente, haz

this.$rootScope.$emit("unauthorized");

más

angular
    .module('foo')
    .run(function($rootScope, $state) {
        $rootScope.$on('unauthorized', () => {
            $state.transitionTo('login');
        });
    });
Stephen Friedrich
fuente
2
Creo que esta es una solución más elegante, ya que no tendrá ninguna dependencia, también podemos escuchar este evento en muchos lugares donde sea relevante
Basav
Esto no funcionará para mis necesidades, ya que no puedo tener un valor de retorno después de enviar un evento.
xabitrigo
13

La mala lógica hizo tales resultados

En realidad, no tiene sentido buscar si el usuario es autor o no en Http Interceptor. Recomendaría envolver todas sus solicitudes HTTP en un solo .service (o .factory, o en .provider), y usarlo para TODAS las solicitudes. Cada vez que llame a la función, puede verificar si el usuario inició sesión o no. Si todo está bien, permita enviar solicitud.

En su caso, la aplicación Angular enviará la solicitud en cualquier caso, solo verifica la autorización allí, y luego JavaScript enviará la solicitud.

Núcleo de tu problema

myHttpInterceptorse llama bajo $httpProviderinstancia. Tus AuthServiceusos $http, o $resource, y aquí tienes la recursividad de dependencia, o dependencia circular. Si elimina esa dependencia de AuthService, no verá ese error.


Además, como señaló @Pieter Herroelen, podría colocar este interceptor en su módulo module.run, pero esto será más como un truco, no una solución.

Si está preparado para hacer un código limpio y autodescriptivo, debe seguir algunos de los principios SÓLIDOS.

Al menos el principio de Responsabilidad Única le ayudará mucho en tales situaciones.

Roman M. Koss
fuente
5
No creo que esta respuesta esté bien redactada, pero creo que llega a la raíz del problema. El problema con un servicio de autenticación que almacena datos de usuarios actuales y los medios para iniciar sesión (una solicitud http) es que es responsable de dos cosas. Si, en cambio, se divide en un servicio para almacenar datos de usuario actual y otro servicio para iniciar sesión, entonces el interceptor http solo necesita depender del "servicio de usuario actual" y ya no crea una dependencia circular.
Snixtor
@Snixtor ¡Gracias! Necesito aprender más inglés, para ser más claro.
Roman M. Koss
0

Si solo está comprobando el estado de autenticación (isAuthorized ()), recomendaría poner ese estado en un módulo separado, diga "Auth", que solo contiene el estado y no usa $ http en sí.

app.config(['$httpProvider', function ($httpProvider) {
  $httpProvider.interceptors.push(function ($location, Auth) {
    return {
      'request': function (config) {
        if (!Auth.isAuthenticated() && $location.path != '/login') {
          console.log('user is not logged in.');
          $location.path('/login');
        }
        return config;
      }
    }
  })
}])

Módulo de autenticación:

angular
  .module('app')
  .factory('Auth', Auth)

function Auth() {
  var $scope = {}
  $scope.sessionId = localStorage.getItem('sessionId')
  $scope.authorized = $scope.sessionId !== null
  //... other auth relevant data

  $scope.isAuthorized = function() {
    return $scope.authorized
  }

  return $scope
}

(Usé localStorage para almacenar el sessionId en el lado del cliente aquí, pero también puede configurar esto dentro de su AuthService después de una llamada $ http, por ejemplo)

joewhite86
fuente