AngularJS: внедрение службы в HTTP-перехватчик (круговая зависимость)

118

Я пытаюсь написать HTTP-перехватчик для моего приложения AngularJS для обработки аутентификации.

Этот код работает, но я беспокоюсь о том, чтобы вручную внедрить службу, поскольку я думал, что Angular должен обрабатывать это автоматически:

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

То, что я начал делать, но столкнулся с проблемами циклической зависимости:

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

Еще одна причина, по которой я обеспокоен, заключается в том, что раздел о $ http в Angular Docs, кажется, показывает способ «обычным способом» вводить зависимости в перехватчик Http. См. Их фрагмент кода в разделе «Перехватчики»:

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

Куда должен идти приведенный выше код?

Думаю, мой вопрос в том, как правильно это сделать?

Спасибо, и я надеюсь, что мой вопрос был достаточно ясным.

shaunlim
источник
1
Просто из любопытства, какие зависимости - если они есть - где вы используете в своем AuthService? У меня были проблемы с циклической зависимостью с использованием метода запроса в перехватчике http, что привело меня сюда. Я использую angularfire $ firebaseAuth. Когда я удалил из инжектора блок кода, который использует $ route (строка 510), все заработало. Там будет вопрос здесь , но это об использовании $ HTTP в перехватчика. Поехали в мерзавец!
slamborne
Хм, чего бы это ни стоило, AuthService в моем случае зависит от $ window, $ http, $ location, $ q
shaunlim
У меня есть случай, когда при некоторых обстоятельствах повторяется запрос в перехватчике, поэтому существует еще более короткая циклическая зависимость от $http. Я нашел единственный способ обойти это - использовать $injector.get, но было бы здорово узнать, есть ли хороший способ структурировать код, чтобы этого избежать.
Михал Чаремза
1
Взгляните на ответ от @rewritten: github.com/angular/angular.js/issues/2367 , в котором у меня была устранена аналогичная проблема. Он делает следующее: $ http = $ http || $ injector.get ( "$ HTTP"); конечно, вы можете заменить $ http своей службой, которую пытаетесь использовать.
Джонатан

Ответы:

42

У вас есть циклическая зависимость между $ http и вашим AuthService.

То, что вы делаете с помощью $injectorслужбы, решает проблему курицы и яйца, откладывая зависимость $ http от AuthService.

Я считаю, что то, что вы сделали, на самом деле является самым простым способом сделать это.

Вы также можете сделать это:

  • Регистрация перехватчика позже (если сделать это в run()блоке, а не в config()блоке, это уже может помочь). Но можете ли вы гарантировать, что $ http еще не был вызван?
  • «Внедрение» $ http вручную в AuthService, когда вы регистрируете перехватчик, вызывая AuthService.setHttp()или что-то в этом роде.
  • ...
Питер Херролен
источник
15
Как этот ответ решает проблему, я не видел? @shaunlim
Inanc Gumus
1
На самом деле это не решение, это просто указывает на то, что поток алгоритма был плохим.
Roman M. Koss
12
Вы не можете зарегистрировать перехватчик в run()блоке, потому что вы не можете внедрить $ httpProvider в блок выполнения. Это можно сделать только на этапе настройки.
Стивен Фридрих
2
Хороший вопрос относительно круговой ссылки, но в противном случае это не должно быть принятым ответом. Ни один из пунктов не имеет никакого смысла
Николай
65

Это то, чем я закончил

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

Примечание. $injector.getВызовы должны выполняться в методах перехватчика. Если вы попытаетесь использовать их в другом месте, вы по-прежнему будете получать ошибку циклической зависимости в JS.

shaunlim
источник
4
Использование ручного впрыска ($ injector.get ('Auth')) решило проблему. Хорошая работа!
Роберт
Чтобы избежать круговой зависимости, я проверяю, какой URL-адрес вызывается. if (! config.url.includes ('/ oauth / v2 / token') && config.url.includes ('/ api')) {// Вызов службы OAuth}. Поэтому круговой зависимости больше нет. По крайней мере, для меня это сработало;).
Brieuc
Отлично. Это как раз то, что мне нужно, чтобы исправить подобную проблему. Спасибо @shaunlim!
Мартин Чемберлин
Мне не очень нравится это решение, потому что тогда эта служба анонимна и не так проста для тестирования. Намного лучшее решение для инъекции во время выполнения.
kmanzana
Это сработало для меня. По сути, внедрение службы, которая использовала $ http.
Thomas
15

Я думаю, что использование инжектора $ напрямую - это антипаттерн.

Способ разорвать круговую зависимость - использовать событие: вместо добавления $ state введите $ rootScope. Вместо прямого перенаправления сделайте

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

плюс

angular
    .module('foo')
    .run(function($rootScope, $state) {
        $rootScope.$on('unauthorized', () => {
            $state.transitionTo('login');
        });
    });
Стивен Фридрих
источник
2
Я думаю, что это более элегантное решение, так как оно не будет иметь никакой зависимости, мы также можем слушать это событие во многих местах, где это необходимо,
Басав
Это не сработает для моих нужд, так как я не могу получить возвращаемое значение после отправки события.
xabitrigo
13

Плохая логика дала такие результаты

На самом деле нет смысла искать, создан ли пользователь в Http Interceptor или нет. Я бы рекомендовал обернуть все ваши HTTP-запросы в один .service (или .factory, или .provider) и использовать его для ВСЕХ запросов. Каждый раз, когда вы вызываете функцию, вы можете проверить, вошел ли пользователь в систему или нет. Если все в порядке, разрешите отправить запрос.

В вашем случае приложение Angular в любом случае отправит запрос, вы просто проверяете там авторизацию, а после этого JavaScript отправит запрос.

Суть вашей проблемы

myHttpInterceptorвызывается под $httpProviderэкземпляром. Ваше AuthServiceиспользование $http, или $resource, и здесь у вас есть рекурсия зависимостей или циклическая зависимость. Если вы удалите эту зависимость из AuthService, то вы не увидите эту ошибку.


Также, как указал @Pieter Herroelen, вы можете поместить этот перехватчик в свой модуль module.run, но это будет больше похоже на взлом, а не на решение.

Если вы хотите создать чистый и информативный код, вы должны придерживаться некоторых принципов SOLID.

Как минимум принцип единственной ответственности вам очень поможет в таких ситуациях.

Роман М. Косс
источник
5
Я не думаю , что этот ответ хорошо сформулированный, но я делать думаю , что он получает в корень проблемы. Проблема со службой аутентификации, в которой хранятся текущие пользовательские данные и средства входа в систему (HTTP-запрос), заключается в том, что она отвечает за две вещи. Если вместо этого он разделен на одну службу для хранения текущих пользовательских данных и другую службу для входа в систему, тогда перехватчику HTTP нужно будет зависеть только от «службы текущего пользователя», и он больше не будет создавать циклическую зависимость.
Snixtor
@Snixtor Спасибо! Мне нужно больше выучить английский, чтобы быть более понятным.
Роман М. Косс
0

Если вы просто проверяете состояние Auth (isAuthorized ()), я бы рекомендовал поместить это состояние в отдельный модуль, скажем «Auth», который просто хранит состояние и не использует сам $ 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;
      }
    }
  })
}])

Модуль аутентификации:

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
}

(я использовал localStorage для хранения здесь sessionId на стороне клиента, но вы также можете установить это внутри своего AuthService, например, после вызова $ http)

joewhite86
источник