AngularJS: service d'injection dans un intercepteur HTTP (dépendance circulaire)

118

J'essaie d'écrire un intercepteur HTTP pour mon application AngularJS pour gérer l'authentification.

Ce code fonctionne, mais je suis préoccupé par l'injection manuelle d'un service car je pensais qu'Angular est censé gérer cela automatiquement:

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

Ce que j'ai commencé à faire, mais j'ai rencontré des problèmes de dépendance circulaire:

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

Une autre raison pour laquelle je suis préoccupé est que la section sur $ http dans Angular Docs semble montrer un moyen d'obtenir des dépendances injectées de la «manière régulière» dans un intercepteur Http. Voir leur extrait de code sous "Intercepteurs":

// 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');

Où doit aller le code ci-dessus?

Je suppose que ma question est de savoir quelle est la bonne façon de procéder?

Merci et j'espère que ma question était assez claire.

shaunlim
la source
1
Juste par curiosité, quelles dépendances - le cas échéant - que vous utilisez dans votre AuthService? J'avais des problèmes de dépendance circulaire en utilisant la méthode de requête dans l'intercepteur http, ce qui m'a amené ici. J'utilise $ firebaseAuth d'angularfire. Lorsque j'ai supprimé le bloc de code qui utilise $ route de l'injecteur (ligne 510), tout a commencé à fonctionner. Il y a un problème ici mais il s'agit d'utiliser $ http dans l'intercepteur. Off to git!
slamborne
Hm, pour ce que ça vaut, AuthService dans mon cas dépend de $ window, $ http, $ location, $ q
shaunlim
J'ai un cas qui réessaye la demande dans l'intercepteur dans certaines circonstances, il y a donc une dépendance circulaire encore plus courte sur $http. Le seul moyen que j'ai trouvé est de l'utiliser $injector.get, mais ce serait bien de savoir s'il existe un bon moyen de structurer le code pour éviter cela.
Michal Charemza
1
Jetez un œil à la réponse de @rewritten: github.com/angular/angular.js/issues/2367 qui a résolu un problème similaire pour moi. Ce qu'il fait est comme ceci: $ http = $ http || $ injector.get ("$ http"); bien sûr, vous pouvez remplacer $ http par votre propre service que vous essayez d'utiliser.
Jonathan

Réponses:

42

Vous avez une dépendance circulaire entre $ http et votre AuthService.

Ce que vous faites en utilisant le $injectorservice, c'est résoudre le problème de la poule et de l'œuf en retardant la dépendance de $ http sur AuthService.

Je crois que ce que vous avez fait est en fait la manière la plus simple de le faire.

Vous pouvez également le faire en:

  • Enregistrer l'intercepteur plus tard (le faire dans un run()bloc au lieu d'un config()bloc pourrait déjà faire l'affaire). Mais pouvez-vous garantir que $ http n'a pas déjà été appelé?
  • "Injecter" manuellement $ http dans AuthService lorsque vous enregistrez l'intercepteur en appelant AuthService.setHttp()ou quelque chose.
  • ...
Pieter Herroelen
la source
15
Comment cette réponse résout le problème, je n'ai pas vu? @shaunlim
Inanc Gumus
1
En fait, il ne résout pas le problème, il souligne simplement que le flux de l'algorithme était mauvais.
Roman M. Koss
12
Vous ne pouvez pas enregistrer l'intercepteur dans un run()bloc, car vous ne pouvez pas injecter $ httpProvider dans un bloc d'exécution. Vous ne pouvez le faire que dans la phase de configuration.
Stephen Friedrich
2
Bon point concernant la référence circulaire, mais sinon, cela ne devrait pas être une réponse acceptée. Aucun des points n'a de sens
Nikolai
65

C'est ce que j'ai fini par faire

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

Remarque: les $injector.getappels doivent être dans les méthodes de l'intercepteur, si vous essayez de les utiliser ailleurs, vous continuerez à obtenir une erreur de dépendance circulaire dans JS.

shaunlim
la source
4
L'utilisation de l'injection manuelle ($ injector.get ('Auth')) a résolu le problème. Bon travail!
Robert
Pour éviter les dépendances circulaires, je vérifie quelle URL est appelée. if (! config.url.includes ('/ oauth / v2 / token') && config.url.includes ('/ api')) {// Appeler le service OAuth}. Il n'y a donc plus de dépendance circulaire. Au moins pour moi, cela a fonctionné;).
Brieuc
Parfait. C'est exactement ce dont j'avais besoin pour résoudre un problème similaire. Merci @shaunlim!
Martyn Chamberlin
Je n'aime pas vraiment cette solution car ce service est anonyme et pas aussi facile à gérer pour les tests. Une bien meilleure solution pour injecter lors de l'exécution.
kmanzana
Cela a fonctionné pour moi. En gros, injecter le service qui utilisait $ http.
Thomas le
15

Je pense que l'utilisation directe de $ injector est un anti-modèle.

Un moyen de rompre la dépendance circulaire est d'utiliser un événement: au lieu d'injecter $ state, injectez $ rootScope. Au lieu de rediriger directement, faites

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

plus

angular
    .module('foo')
    .run(function($rootScope, $state) {
        $rootScope.$on('unauthorized', () => {
            $state.transitionTo('login');
        });
    });
Stephen Friedrich
la source
2
Je pense que c'est une solution plus élégante, car elle n'aura aucune dépendance, nous pouvons également écouter cet événement à de nombreux endroits le cas échéant
Basav
Cela ne fonctionnera pas pour mes besoins, car je ne peux pas avoir de valeur de retour après l'envoi d'un événement.
xabitrigo
13

La mauvaise logique a fait de tels résultats

En fait, il est inutile de rechercher si l'utilisateur a été créé ou non dans Http Interceptor. Je recommanderais d'envelopper toutes vos demandes HTTP dans un seul .service (ou .factory, ou dans .provider), et de l'utiliser pour TOUTES les demandes. À chaque fois que vous appelez la fonction, vous pouvez vérifier si l'utilisateur est connecté ou non. Si tout va bien, autorisez l'envoi de la demande.

Dans votre cas, l'application Angular enverra une demande dans tous les cas, il vous suffit de vérifier l'autorisation là-bas, et après cela, JavaScript enverra la demande.

Au cœur de votre problème

myHttpInterceptorest appelé sous $httpProviderinstance. Vos AuthServiceutilisations $http, ou $resource, et ici vous avez une récursivité de dépendance ou une dépendance circulaire. Si vous supprimez cette dépendance de AuthService, vous ne verrez pas cette erreur.


De plus, comme @Pieter Herroelen l'a souligné, vous pouvez placer cet intercepteur dans votre module module.run, mais ce sera plus comme un hack, pas une solution.

Si vous êtes prêt à faire du code propre et auto-descriptif, vous devez suivre certains des principes SOLID.

Au moins le principe de responsabilité unique vous aidera beaucoup dans de telles situations.

Roman M. Koss
la source
5
Je ne pense pas que cette réponse est bien formulée, mais je ne pense qu'il arrive à la racine du problème. Le problème avec un service d'authentification qui stocke les données utilisateur actuelles et les moyens de connexion (une requête http) est qu'il est responsable de deux choses. Si cela est plutôt divisé en un service pour stocker les données utilisateur actuelles et un autre service pour se connecter, alors l'intercepteur http doit uniquement dépendre du «service utilisateur actuel» et ne crée plus de dépendance circulaire.
Snixtor
@Snixtor Merci! J'ai besoin d'apprendre l'anglais plus, pour être plus clair.
Roman M. Koss
0

Si vous ne faites que vérifier l'état d'authentification (isAuthorized ()), je recommanderais de mettre cet état dans un module séparé, dites "Auth", qui contient simplement l'état et n'utilise pas $ http lui-même.

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

Module d'authentification:

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
}

(J'ai utilisé localStorage pour stocker le sessionId côté client ici, mais vous pouvez également le définir dans votre AuthService après un appel $ http par exemple)

joewhite86
la source