AngularJS: wstrzykiwanie usługi do przechwytywacza HTTP (zależność cykliczna)

118

Próbuję napisać przechwytywacz HTTP dla mojej aplikacji AngularJS do obsługi uwierzytelniania.

Ten kod działa, ale obawiam się ręcznego wstrzykiwania usługi, ponieważ pomyślałem, że Angular powinien obsługiwać to automatycznie:

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

Co zacząłem robić, ale napotkałem problemy z zależnością cykliczną:

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

Innym powodem, dla którego jestem zaniepokojony, jest to, że sekcja dotycząca $ http w Angular Docs wydaje się pokazywać sposób na wprowadzenie zależności w „zwykły sposób” do przechwytywacza HTTP. Zobacz ich fragment kodu w sekcji „Interceptory”:

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

Gdzie powinien iść powyższy kod?

Myślę, że moje pytanie brzmi: jaki jest właściwy sposób, aby to zrobić?

Dziękuję i mam nadzieję, że moje pytanie było wystarczająco jasne.

shaunlim
źródło
1
Tak z ciekawości, jakich zależności - jeśli w ogóle - używasz w AuthService? Miałem problemy z zależnością cykliczną przy użyciu metody żądania w przechwytywaczu http, która przyniosła mnie tutaj. Używam $ firebaseAuth firmy Angularfire. Kiedy usunąłem blok kodu, który używa $ route z wtryskiwacza (linia 510), wszystko zaczęło działać. Jest tu problem , ale chodzi o użycie $ http w przechwytywaczu. Do dupy!
slamborne
Hm, na ile to jest warte, AuthService w moim przypadku zależy od $ window, $ http, $ location, $ q
shaunlim
Mam przypadek, który w pewnych okolicznościach ponawia żądanie w przechwytywaczu, więc istnieje jeszcze krótsza zależność cykliczna od $http. Jedynym sposobem obejścia tego problemu jest użycie $injector.get, ale byłoby wspaniale wiedzieć, czy istnieje dobry sposób na uporządkowanie kodu, aby tego uniknąć.
Michał Charemza
1
Spójrz na odpowiedź od @rewritten: github.com/angular/angular.js/issues/2367, która rozwiązała podobny problem. Robi tak: $ http = $ http || $ injector.get ("$ http"); oczywiście możesz zamienić $ http na własną usługę, której próbujesz użyć.
Jonathan

Odpowiedzi:

42

Masz cykliczną zależność między $ http a usługą AuthService.

To, co robisz, korzystając z $injectorusługi, polega na rozwiązaniu problemu z kura i jajkiem przez opóźnienie zależności $ http od usługi AuthService.

Uważam, że to, co zrobiłeś, jest właściwie najprostszym sposobem na zrobienie tego.

Możesz to również zrobić poprzez:

  • Rejestracja przechwytywacza później (robienie tego w run()bloku zamiast w config()bloku może już załatwić sprawę). Ale czy możesz zagwarantować, że $ http nie zostało jeszcze wywołane?
  • „Wstrzykiwanie” $ http ręcznie do usługi AuthService podczas rejestrowania przechwytywacza przez wywołanie AuthService.setHttp()lub coś w tym stylu.
  • ...
Pieter Herroelen
źródło
15
Jak ta odpowiedź rozwiązuje problem, nie widziałem? @shaunlim
Inanc Gumus
1
Właściwie nie rozwiązuje tego problemu, po prostu wskazuje, że przepływ algorytmu był zły.
Roman M. Koss,
12
Nie możesz zarejestrować przechwytywacza w run()bloku, ponieważ nie możesz wstrzyknąć $ httpProvider do bloku uruchamiania. Możesz to zrobić tylko w fazie konfiguracji.
Stephen Friedrich
2
Słuszna uwaga, ponowne odniesienie cykliczne, ale poza tym nie powinna być akceptowaną odpowiedzią. Żaden z punktorów nie ma sensu
Nikolai
65

Oto, co ostatecznie zrobiłem

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

Uwaga: $injector.getwywołania powinny znajdować się w metodach przechwytywacza, jeśli spróbujesz ich użyć w innym miejscu, nadal będziesz otrzymywać cykliczny błąd zależności w JS.

shaunlim
źródło
4
Użycie ręcznego wtrysku ($ injector.get ('Auth')) rozwiązało problem. Dobra robota!
Robert
Aby uniknąć zależności cyklicznych, sprawdzam, który adres URL jest wywoływany. if (! config.url.includes ('/ oauth / v2 / token') && config.url.includes ('/ api')) {// Call OAuth Service}. Dlatego nie ma już zależności cyklicznej. Przynajmniej u mnie zadziałało;).
Brieuc
Idealny. Właśnie tego potrzebowałem, aby naprawić podobny problem. Dzięki @shaunlim!
Martyn Chamberlin
Nie podoba mi się to rozwiązanie, ponieważ ta usługa jest anonimowa i nie jest tak łatwa do opanowania podczas testów. O wiele lepsze rozwiązanie do wstrzykiwania w czasie wykonywania.
kmanzana
To zadziałało dla mnie. Zasadniczo wstrzyknięcie usługi używającej $ http.
Thomas
15

Myślę, że użycie wtryskiwacza $ bezpośrednio jest przeciwieństwem.

Sposobem na przerwanie zależności cyklicznej jest użycie zdarzenia: zamiast wstrzykiwać $ state, wstrzyknij $ rootScope. Zamiast przekierowywać bezpośrednio, zrób

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

plus

angular
    .module('foo')
    .run(function($rootScope, $state) {
        $rootScope.$on('unauthorized', () => {
            $state.transitionTo('login');
        });
    });
Stephen Friedrich
źródło
2
Myślę, że jest to bardziej eleganckie rozwiązanie, ponieważ nie będzie miało żadnej zależności, tego wydarzenia możemy również posłuchać w wielu miejscach, gdziekolwiek jest to istotne
Basav
To nie zadziała dla moich potrzeb, ponieważ nie mogę mieć wartości zwracanej po wysłaniu zdarzenia.
xabitrigo
13

Zła logika doprowadziła do takich wyników

Właściwie nie ma sensu szukać, czy autorstwo użytkownika jest czy nie w Http Interceptor. Zalecałbym zawinięcie wszystkich żądań HTTP w pojedynczy plik .service (lub .factory lub .provider) i użycie go dla WSZYSTKICH żądań. Za każdym razem, gdy wywołujesz funkcję, możesz sprawdzić, czy użytkownik jest zalogowany, czy nie. Jeśli wszystko jest w porządku, zezwól na wysyłanie żądania.

W Twoim przypadku aplikacja Angular wyśle ​​żądanie w każdym przypadku, po prostu sprawdzasz tam autoryzację, a następnie JavaScript wyśle ​​żądanie.

Sedno twojego problemu

myHttpInterceptorjest wywoływana pod $httpProviderinstancją. Twoje AuthServicezastosowania $httplub $resource, a tutaj masz rekursję zależności lub zależność cykliczną. Jeśli usuniesz tę zależność zAuthService , nie zobaczysz tego błędu.


Jak zauważył @Pieter Herroelen, możesz umieścić ten przechwytywacz w swoim module module.run , ale będzie to bardziej jak włamanie, a nie rozwiązanie.

Jeśli jesteś w stanie zrobić czysty i samoopisowy kod, musisz przestrzegać niektórych zasad SOLID.

Przynajmniej zasada pojedynczej odpowiedzialności bardzo Ci w takich sytuacjach pomoże.

Roman M. Koss
źródło
5
Nie sądzę, że ta odpowiedź jest dobrze sformułowany, ale zrobić , że to dostaje się do źródła problemu. Problem z usługą autoryzacji, która przechowuje aktualne dane użytkownika i sposób logowania (żądanie http) polega na tym, że odpowiada ona za dwie rzeczy. Jeśli zamiast tego zostanie podzielony na jedną usługę do przechowywania bieżących danych użytkownika i inną usługę do logowania, wówczas przechwytywacz http musi polegać tylko na „bieżącej usłudze użytkownika” i nie tworzy już cyklicznej zależności.
Snixtor
@Snixtor Dziękuję! Muszę więcej nauczyć się angielskiego, żeby było bardziej zrozumiałe.
Roman M. Koss
0

Jeśli sprawdzasz tylko stan Auth (isAuthorized ()), zalecałbym umieszczenie tego stanu w osobnym module, powiedz „Auth”, który po prostu przechowuje stan i nie używa samego $ http.

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

Moduł autoryzacji:

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
}

(Użyłem localStorage do przechowywania sessionId po stronie klienta tutaj, ale możesz również ustawić to w swoim AuthService na przykład po wywołaniu $ http)

joewhite86
źródło