AngularJS: Injizieren eines Dienstes in einen HTTP-Interceptor (zirkuläre Abhängigkeit)

117

Ich versuche, einen HTTP-Interceptor für meine AngularJS-App zu schreiben, um die Authentifizierung zu handhaben.

Dieser Code funktioniert, aber ich mache mir Sorgen um das manuelle Einfügen eines Dienstes, da ich dachte, Angular sollte dies automatisch erledigen:

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

Was ich anfing zu tun, aber auf zirkuläre Abhängigkeitsprobleme stieß:

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

Ein weiterer Grund, warum ich besorgt bin, ist, dass der Abschnitt über $ http in den Angular Docs einen Weg zu zeigen scheint, wie Abhängigkeiten auf "normale Weise" in einen HTTP-Interceptor injiziert werden können. Siehe ihren Code-Ausschnitt unter "Interceptors":

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

Wohin soll der obige Code gehen?

Ich denke, meine Frage ist, wie man das richtig macht.

Danke, und ich hoffe, meine Frage war klar genug.

shaunlim
quelle
1
Welche Abhängigkeiten - falls vorhanden - verwenden Sie aus Neugier in Ihrem AuthService? Ich hatte zirkuläre Abhängigkeitsprobleme mit der Anforderungsmethode im http-Interceptor, die mich hierher gebracht haben. Ich verwende Angularfires $ firebaseAuth. Als ich den Codeblock, der $ route verwendet, aus dem Injektor entfernte (Zeile 510), fing alles an zu funktionieren. Hier gibt es ein Problem , aber es geht darum, $ http im Interceptor zu verwenden. Auf zum Trottel!
Slamborne
Hm, für das, was es wert ist, hängt AuthService in meinem Fall von $ window, $ http, $ location, $ q
shaunlim
Ich habe einen Fall, in dem die Anforderung unter bestimmten Umständen im Interceptor wiederholt wird, sodass eine noch kürzere zirkuläre Abhängigkeit von besteht $http. Der einzige Weg, den ich gefunden habe, ist die Verwendung $injector.get, aber es wäre toll zu wissen, ob es eine gute Möglichkeit gibt, den Code zu strukturieren, um dies zu vermeiden.
Michal Charemza
1
Schauen Sie sich die Antwort von @rewritten an: github.com/angular/angular.js/issues/2367 , die ein ähnliches Problem für mich behoben hat. Was er tut, ist wie folgt: $ http = $ http || $ Injektor.get ("$ http"); Natürlich können Sie $ http durch Ihren eigenen Dienst ersetzen, den Sie verwenden möchten.
Jonathan

Antworten:

41

Sie haben eine zirkuläre Abhängigkeit zwischen $ http und Ihrem AuthService.

Mit dem $injectorDienst lösen Sie das Henne-Ei-Problem, indem Sie die Abhängigkeit von $ http vom AuthService verzögern.

Ich glaube, dass das, was Sie getan haben, der einfachste Weg ist, es zu tun.

Sie können dies auch tun, indem Sie:

  • Später den Interceptor registrieren (dies in einem run()Block anstelle eines config()Blocks zu tun, reicht möglicherweise bereits aus). Aber können Sie garantieren, dass $ http noch nicht aufgerufen wurde?
  • "Injizieren" von $ http manuell in den AuthService, wenn Sie den Interceptor durch Aufrufen AuthService.setHttp()oder ähnliches registrieren .
  • ...
Pieter Herroelen
quelle
15
Wie diese Antwort das Problem löst, habe ich nicht gesehen? @shaunlim
Inanc Gumus
1
Eigentlich löst es es nicht, es weist nur darauf hin, dass der Algorithmusfluss schlecht war.
Roman M. Koss
12
Sie können den Interceptor nicht in einem run()Block registrieren , da Sie $ httpProvider nicht in einen Ausführungsblock einfügen können. Dies können Sie nur in der Konfigurationsphase tun.
Stephen Friedrich
2
Guter Punkt bezüglich Zirkelverweis, aber ansonsten sollte es keine akzeptierte Antwort sein. Keiner der Aufzählungspunkte macht irgendeinen Sinn
Nikolai
64

Das habe ich letztendlich getan

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

Hinweis: Die $injector.getAufrufe sollten innerhalb der Methoden des Interceptors liegen. Wenn Sie versuchen, sie an anderer Stelle zu verwenden, wird in JS weiterhin ein zirkulärer Abhängigkeitsfehler angezeigt.

shaunlim
quelle
4
Die Verwendung der manuellen Injektion ($ injizor.get ('Auth')) löste das Problem. Gute Arbeit!
Robert
Um zirkuläre Abhängigkeiten zu vermeiden, überprüfe ich, welche URL aufgerufen wird. if (! config.url.includes ('/ oauth / v2 / token') && config.url.includes ('/ api')) {// OAuth Service aufrufen}. Daher gibt es keine zirkuläre Abhängigkeit mehr. Zumindest für mich hat es geklappt;).
Brieuc
Perfekt. Genau das brauchte ich, um ein ähnliches Problem zu beheben. Danke @shaunlim!
Martyn Chamberlin
Ich mag diese Lösung nicht wirklich, weil dieser Service dann anonym ist und nicht so einfach für Tests in den Griff zu bekommen ist. Viel bessere Lösung zur Laufzeit.
kmanzana
Das hat bei mir funktioniert. Grundsätzlich wird der Dienst injiziert, der $ http verwendet.
Thomas
15

Ich denke, die direkte Verwendung des $ -Injektors ist ein Antimuster.

Eine Möglichkeit, die zirkuläre Abhängigkeit zu überwinden, besteht darin, ein Ereignis zu verwenden: Anstatt $ state zu injizieren, injizieren Sie $ rootScope. Anstatt direkt umzuleiten, tun Sie dies

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

Plus

angular
    .module('foo')
    .run(function($rootScope, $state) {
        $rootScope.$on('unauthorized', () => {
            $state.transitionTo('login');
        });
    });
Stephen Friedrich
quelle
2
Ich denke, dies ist eine elegantere Lösung, da sie keine Abhängigkeit haben wird. Wir können dieses Ereignis auch an vielen Orten anhören, wo immer dies relevant ist
Basav
Dies funktioniert nicht für meine Anforderungen, da ich nach dem Auslösen eines Ereignisses keinen Rückgabewert haben kann.
Xabitrigo
13

Eine schlechte Logik führte zu solchen Ergebnissen

Tatsächlich gibt es keinen Grund zu suchen, ob der Benutzer in Http Interceptor verfasst ist oder nicht. Ich würde empfehlen, alle HTTP-Anforderungen in einen einzelnen .service (oder .factory oder in .provider) zu verpacken und für ALLE Anforderungen zu verwenden. Bei jedem Aufruf der Funktion können Sie überprüfen, ob der Benutzer angemeldet ist oder nicht. Wenn alles in Ordnung ist, erlauben Sie die Sendeanforderung.

In Ihrem Fall sendet die Angular-Anwendung in jedem Fall eine Anfrage, Sie überprüfen nur die Autorisierung dort und danach sendet JavaScript eine Anfrage.

Kern Ihres Problems

myHttpInterceptorwird unter $httpProviderInstanz aufgerufen . Ihre AuthServiceVerwendungen $httpoder $resource, und hier haben Sie eine Abhängigkeitsrekursion oder eine zirkuläre Abhängigkeit. Wenn Sie diese Abhängigkeit entfernen, AuthServicewird dieser Fehler nicht angezeigt.


Auch wie @Pieter Herroelen betonte, könnten Sie diesen Interceptor in Ihr Modul einfügen module.run, aber dies ist eher ein Hack, keine Lösung.

Wenn Sie sauberen und selbstbeschreibenden Code erstellen möchten, müssen Sie einige der SOLID-Prinzipien einhalten.

Zumindest das Prinzip der Einzelverantwortung wird Ihnen in solchen Situationen sehr helfen.

Roman M. Koss
quelle
5
Ich denke nicht, dass diese Antwort gut formuliert ist, aber ich denke, dass sie die Wurzel des Problems erreicht. Das Problem mit einem Authentifizierungsdienst, der aktuelle Benutzerdaten speichert und sich anmeldet (eine http-Anfrage), besteht darin, dass er für zwei Dinge verantwortlich ist. Wenn dies stattdessen in einen Dienst zum Speichern aktueller Benutzerdaten und einen anderen Dienst zum Anmelden unterteilt ist, muss der http-Interceptor nur vom "aktuellen Benutzerdienst" abhängen und erstellt keine zirkuläre Abhängigkeit mehr.
Snixtor
@ Snixtor Danke! Ich muss mehr Englisch lernen, um klarer zu sein.
Roman M. Koss
0

Wenn Sie nur nach dem Auth-Status suchen (isAuthorized ()), würde ich empfehlen, diesen Status in ein separates Modul zu setzen, z. B. "Auth", das nur den Status enthält und $ http selbst nicht verwendet.

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

Auth-Modul:

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
}

(Ich habe localStorage verwendet, um die Sitzungs-ID hier auf der Clientseite zu speichern. Sie können dies jedoch auch nach einem Aufruf von $ http in Ihrem AuthService festlegen.)

joewhite86
quelle