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.

Shaunlim
fonte
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.
  • ...
Pieter Herroelen
fonte
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.

Shaunlim
fonte
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');
        });
    });
Stephen Friedrich
fonte
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.

Roman M. Koss
fonte
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)

joewhite86
fonte