AngularJS: inserimento del servizio in un intercettore HTTP (dipendenza circolare)


118

Sto cercando di scrivere un intercettore HTTP per la mia app AngularJS per gestire l'autenticazione.

Questo codice funziona, ma sono preoccupato per l'inserimento manuale di un servizio poiché pensavo che Angular dovesse gestirlo automaticamente:

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

Quello che ho iniziato a fare, ma mi sono imbattuto in problemi di dipendenza circolare:

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

Un altro motivo per cui sono preoccupato è che la sezione su $ http in Angular Docs sembra mostrare un modo per ottenere le dipendenze iniettate in "modo regolare" in un intercettore Http. Vedi lo snippet di codice in "Interceptor":

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

Dove dovrebbe andare il codice sopra?

Immagino che la mia domanda sia qual è il modo giusto per farlo?

Grazie, e spero che la mia domanda sia stata abbastanza chiara.


1
Solo per curiosità, quali dipendenze - se ce ne sono - in cui utilizzi nel tuo AuthService? Stavo avendo problemi di dipendenza circolare utilizzando il metodo di richiesta nell'interceptor http, che mi ha portato qui. Sto usando $ firebaseAuth di angularfire. Quando ho rimosso il blocco di codice che utilizza $ route dall'iniettore (riga 510), tutto ha iniziato a funzionare. C'è un problema qui, ma si tratta di utilizzare $ http nell'interceptor. Fuori a git!
slamborne

Hm, per quello che vale, AuthService nel mio caso dipende da $ window, $ http, $ location, $ q
shaunlim

Ho un caso che ritenta la richiesta nell'interceptor in alcune circostanze, quindi esiste una dipendenza circolare ancora più breve da $http. L'unico modo per aggirare il problema è quello di usarlo $injector.get, ma sarebbe bello sapere se esiste un buon modo per strutturare il codice per evitarlo.
Michal Charemza

1
Dai un'occhiata alla risposta di @rewritten: github.com/angular/angular.js/issues/2367 che ha risolto un problema simile per me. Quello che sta facendo è così: $ http = $ http || $ injector.get ( "$ http"); ovviamente puoi sostituire $ http con il tuo servizio che stai cercando di utilizzare.
Jonathan

Risposte:


42

Hai una dipendenza circolare tra $ http e il tuo AuthService.

Quello che stai facendo usando il $injectorservizio è risolvere il problema dell'uovo e della gallina ritardando la dipendenza di $ http su AuthService.

Credo che quello che hai fatto sia in realtà il modo più semplice per farlo.

Puoi anche farlo:

  • Registrare l'interceptor in un secondo momento (farlo in un run()blocco invece che in un config()blocco potrebbe già fare il trucco). Ma puoi garantire che $ http non sia già stato chiamato?
  • "Iniettare" $ http manualmente in AuthService quando registri l'interceptor chiamando AuthService.setHttp()o qualcosa del genere.
  • ...

15
Come questa risposta sta risolvendo il problema, non ho visto? @shaunlim
Gumus

1
In realtà non lo sta risolvendo, indica solo che il flusso dell'algoritmo è andato male.
Roman M. Koss

12
Non è possibile registrare l'interceptor in un run()blocco, poiché non è possibile iniettare $ httpProvider in un blocco di esecuzione. Puoi farlo solo in fase di configurazione.
Stephen Friedrich

2
Buon punto riguardo al riferimento circolare, ma per il resto non dovrebbe essere una risposta accettata. Nessuno dei punti elenco ha alcun senso
Nikolai

65

Questo è quello che ho finito per fare

  .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: le $injector.getchiamate dovrebbero essere all'interno dei metodi dell'interceptor, se provi a usarle altrove continuerai a ricevere un errore di dipendenza circolare in JS.


4
Risolto il problema con l'iniezione manuale ($ injector.get ('Auth')). Buon lavoro!
Robert

Per evitare dipendenze circolari sto controllando quale URL è chiamato. if (! config.url.includes ('/ oauth / v2 / token') && config.url.includes ('/ api')) {// Call OAuth Service}. Quindi non c'è più dipendenza circolare. Almeno per me ha funzionato;).
Brieuc

Perfetto. Questo è esattamente ciò di cui avevo bisogno per risolvere un problema simile. Grazie @shaunlim!
Martyn Chamberlin

Non mi piace molto questa soluzione perché quindi questo servizio è anonimo e non è così facile da gestire per i test. Soluzione molto migliore da iniettare in runtime.
kmanzana

Ha funzionato per me. Fondamentalmente iniettando il servizio che utilizzava $ http.
Thomas

15

Penso che l'uso diretto di $ injector sia un antipattern.

Un modo per interrompere la dipendenza circolare è usare un evento: invece di iniettare $ state, iniettare $ rootScope. Invece di reindirizzare direttamente, fallo

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

più

angular
    .module('foo')
    .run(function($rootScope, $state) {
        $rootScope.$on('unauthorized', () => {
            $state.transitionTo('login');
        });
    });

2
Penso che questa sia una soluzione più elegante, poiché non avrà alcuna dipendenza, possiamo anche ascoltare questo evento in molti luoghi, ovunque sia pertinente
Basav

Questo non funzionerà per le mie esigenze, poiché non posso avere un valore di ritorno dopo aver inviato un evento.
xabitrigo

13

Una cattiva logica ha prodotto tali risultati

In realtà non ha senso cercare se l'utente è autore o meno in Http Interceptor. Suggerirei di racchiudere tutte le tue richieste HTTP in un singolo .service (o .factory o .provider) e di usarlo per TUTTE le richieste. Ogni volta che si richiama la funzione, è possibile verificare se l'utente ha effettuato l'accesso o meno. Se tutto va bene, consenti invio richiesta.

Nel tuo caso, l'applicazione Angular invierà la richiesta in ogni caso, devi solo controllare l'autorizzazione lì, e dopo che JavaScript invierà la richiesta.

Al centro del tuo problema

myHttpInterceptorè chiamato sotto $httpProvideristanza. I tuoi AuthServiceusi $http, o $resource, e qui hai la ricorsione delle dipendenze, o dipendenza circolare. Se rimuovi quella dipendenza da AuthService, non vedrai quell'errore.


Inoltre, come ha sottolineato @Pieter Herroelen, potresti inserire questo intercettore nel tuo modulo module.run, ma questo sarà più come un hack, non una soluzione.

Se sei all'altezza del codice pulito e auto descrittivo, devi seguire alcuni dei principi SOLIDI.

Almeno il principio della responsabilità unica ti aiuterà molto in tali situazioni.


5
Non credo che questa risposta è ben formulato, ma io farlo credo che arriva alla radice del problema. Il problema con un servizio di autenticazione che memorizza i dati utente correnti e i mezzi di accesso (una richiesta http) è che è responsabile di due cose. Se questo è invece diviso in un servizio per l'archiviazione dei dati dell'utente corrente e un altro servizio per l'accesso, l'interceptor http deve dipendere solo dal "servizio utente corrente" e non crea più una dipendenza circolare.
Snixtor

@Snixtor Grazie! Ho bisogno di imparare di più l'inglese, per essere più chiaro.
Roman M. Koss

0

Se stai solo controllando lo stato di autenticazione (isAuthorized ()), consiglierei di mettere quello stato in un modulo separato, dire "Auth", che mantiene solo lo stato e non usa $ http stesso.

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

Modulo di autenticazione:

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
}

(ho usato localStorage per memorizzare il sessionId sul lato client qui, ma puoi anche impostarlo all'interno del tuo AuthService dopo una chiamata $ http, ad esempio)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.