AngularJS: injeção de serviço em um interceptor HTTP (dependência circular)


118

Estou tentando escrever um interceptor HTTP para meu aplicativo AngularJS para lidar com a autenticação.

Este código funciona, mas estou preocupado em injetar manualmente um serviço, pois pensei que o Angular deveria lidar com isso 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;
            }
        };
    })
}]);

O que comecei fazendo, mas encontrei problemas de dependência 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');
});

Outra razão pela qual estou preocupado é que a seção sobre $ http no Angular Docs parece mostrar uma maneira de obter dependências injetadas de "maneira normal" em um interceptor Http. Veja o snippet de código em "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');

Para onde deve ir o código acima?

Acho que minha pergunta é qual é a maneira certa de fazer isso?

Obrigado, e espero que minha pergunta tenha sido clara o suficiente.


1
Só por curiosidade, quais dependências - se houver - em seu AuthService? Eu estava tendo problemas de dependência circular usando o método de solicitação no interceptor http, o que me trouxe até aqui. Estou usando $ firebaseAuth do angularfire. Quando removi o bloco de código que usa $ route do injetor (linha 510), tudo começou a funcionar. Há um problema aqui, mas é sobre o uso de $ http no interceptor. Vá para o git!
— slamborne

Hm, pelo que vale a pena, AuthService no meu caso depende de $ window, $ http, $ location, $ q
— shaunlim

Eu tenho um caso que tenta novamente a solicitação no interceptor em algumas circunstâncias, portanto, há uma dependência circular ainda mais curta em $http. A única maneira de contornar isso que encontrei é usar $injector.get, mas seria ótimo saber se existe uma boa maneira de estruturar o código para evitar isso.
— Michal Charemza

1
Dê uma olhada na resposta de @rewritten: github.com/angular/angular.js/issues/2367 que corrigiu um problema semelhante para mim. O que ele está fazendo é assim: $ http = $ http || $ injector.get ("$ http"); é claro que você pode substituir $ http pelo seu próprio serviço que está tentando usar.
— Jonathan

Respostas:


42

Você tem uma dependência circular entre $ http e seu AuthService.

O que você está fazendo ao usar o $injectorserviço é resolver o problema do ovo e da galinha atrasando a dependência de $ http no AuthService.

Acredito que o que você fez é na verdade a maneira mais simples de fazer.

Você também pode fazer isso:

  • Registrar o interceptor mais tarde (fazer isso em um run()bloco em vez de em um config()bloco já pode funcionar). Mas você pode garantir que $ http ainda não foi chamado?
  • "Injetando" $ http manualmente no AuthService quando você está registrando o interceptor chamando AuthService.setHttp()ou algo assim.
  • ...

15
Como essa resposta está resolvendo o problema, eu não vi? @shaunlim
— Inanc Gumus

1
Na verdade não está resolvendo, apenas aponta que o fluxo do algoritmo estava ruim.
— Roman M. Koss

12
Você não pode registrar o interceptor em um run()bloco, porque você não pode injetar $ httpProvider em um bloco de execução. Você só pode fazer isso na fase de configuração.
— Stephen Friedrich

2
Bom ponto em relação à referência circular, mas caso contrário, não deve ser uma resposta aceita. Nenhum dos marcadores faz sentido
— Nikolai

65

Isso é o que eu acabei fazendo

  .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: As $injector.getchamadas devem estar dentro dos métodos do interceptor; se você tentar usá-las em outro lugar, continuará a obter um erro de dependência circular em JS.


4
Usando injeção manual ($ injector.get ('Auth')) resolveu o problema. Bom trabalho!
— Robert

Para evitar a dependência circular, estou verificando qual url é chamada. if (! config.url.includes ('/ oauth / v2 / token') && config.url.includes ('/ api')) {// Chame o serviço OAuth}. Portanto, não há mais dependência circular. Pelo menos para mim funcionou;).
— Brieuc

Perfeito. Isso é exatamente o que eu precisava para consertar um problema semelhante. Obrigado @shaunlim!
— Martyn Chamberlin de

Eu realmente não gosto dessa solução porque esse serviço é anônimo e não é tão fácil de controlar para testes. Solução muito melhor para injetar em tempo de execução.
— kmanzana

Isso funcionou para mim. Basicamente injetando o serviço que estava usando $ http.
— Thomas

15

Acho que usar o injetor $ diretamente é um antipadrão.

Uma maneira de quebrar a dependência circular é usar um evento: em vez de injetar $ state, injete $ rootScope. Em vez de redirecionar diretamente, faça

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

mais

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

2
Acho que esta é uma solução mais elegante, pois não terá nenhuma dependência, também podemos ouvir este evento em muitos lugares onde for relevante
— Basav

Isso não funcionará para minhas necessidades, pois não posso ter um valor de retorno após despachar um evento.
— xabitrigo

13

A má lógica gerou tais resultados

Na verdade não adianta buscar se é de autoria do usuário ou não no Http Interceptor. Eu recomendaria agrupar todas as suas solicitações HTTP em um único .service (ou .factory, ou em .provider) e usá-lo para TODAS as solicitações. Cada vez que você chamar a função, você pode verificar se o usuário está logado ou não. Se tudo estiver ok, permita o envio de solicitação.

No seu caso, o aplicativo Angular irá enviar a solicitação em qualquer caso, você apenas checando a autorização lá, e depois disso o JavaScript irá enviar a solicitação.

Cerne do seu problema

myHttpInterceptoré chamado em $httpProviderinstância. Seus AuthServiceusos $http, ou $resource, e aqui você tem recursão de dependência ou dependência circular. Se você remover essa dependência AuthService, não verá esse erro.


Além disso, como @Pieter Herroelen apontou, você poderia colocar este interceptor em seu módulo module.run, mas isso será mais como um hack, não uma solução.

Se você quer fazer um código limpo e autodescritivo, deve seguir alguns dos princípios SOLID.

Pelo menos o princípio da Responsabilidade Única o ajudará muito nessas situações.


5
Eu não acho que esta resposta está bem redigido, mas eu fazer pensar que ele chegue à raiz do problema. O problema com um serviço de autenticação que armazena dados atuais do usuário e os meios de login (uma solicitação http) é que ele é responsável por duas coisas. Se, em vez disso, for dividido em um serviço para armazenar dados do usuário atual e outro serviço para efetuar login, o interceptor http precisará depender apenas do "serviço do usuário atual" e não criará mais uma dependência circular.
— Snixtor de

@Snixtor Obrigado! Preciso aprender mais inglês, para ser mais claro.
— Roman M. Koss

0

Se você está apenas verificando o estado Auth (isAuthorized ()), eu recomendaria colocar esse estado em um módulo separado, diga "Auth", que apenas mantém o estado e não usa $ http em si.

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 autenticação:

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
}

(usei localStorage para armazenar o sessionId no lado do cliente aqui, mas você também pode definir isso dentro do seu AuthService após uma chamada $ http, por exemplo)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.