AngularJS: Layanan injeksi ke interseptor HTTP (ketergantungan melingkar)


118

Saya mencoba menulis pencegat HTTP untuk aplikasi AngularJS saya untuk menangani otentikasi.

Kode ini berfungsi, tetapi saya khawatir tentang menyuntikkan layanan secara manual karena saya pikir Angular seharusnya menangani ini secara otomatis:

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

Apa yang saya mulai lakukan, tetapi mengalami masalah ketergantungan melingkar:

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

Alasan lain mengapa saya khawatir adalah bahwa bagian di $ http di Angular Docs tampaknya menunjukkan cara untuk memasukkan dependensi dengan "cara biasa" ke dalam pencegat Http. Lihat cuplikan kodenya di bawah "Interceptors":

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

Kemana perginya kode di atas?

Saya kira pertanyaan saya adalah apa cara yang benar untuk melakukan ini?

Terima kasih, dan saya harap pertanyaan saya cukup jelas.


1
Hanya ingin tahu, dependensi apa - jika ada - yang Anda gunakan di AuthService? Saya mengalami masalah ketergantungan melingkar menggunakan metode permintaan di http interseptor, yang membawa saya ke sini. Saya menggunakan $ firebaseAuth dari angularfire. Ketika saya menghapus blok kode yang menggunakan $ route dari injektor (baris 510), semuanya mulai bekerja. Ada masalah di sini tetapi tentang menggunakan $ http di pencegat. Berangkat!
slamborne

Hm, untuk apa nilainya, AuthService dalam kasus saya bergantung pada $ window, $ http, $ location, $ q
shaunlim

Saya punya kasus yang mencoba ulang permintaan di interseptor dalam beberapa keadaan, jadi ada ketergantungan melingkar yang lebih pendek pada $http. Satu-satunya cara untuk mengatasinya yang saya temukan adalah dengan menggunakan $injector.get, tetapi akan sangat bagus untuk mengetahui apakah ada cara yang baik untuk menyusun kode untuk menghindari hal ini.
Michal Charemza

1
Lihatlah tanggapan dari @rewritten: github.com/angular/angular.js/issues/2367 yang memperbaiki masalah serupa untuk saya. Apa yang dia lakukan seperti ini: $ http = $ http || $ injector.get ("$ http"); tentu saja Anda dapat mengganti $ http dengan layanan Anda sendiri yang Anda coba gunakan.
Jonathan

Jawaban:


42

Anda memiliki ketergantungan melingkar antara $ http dan AuthService Anda.

Apa yang Anda lakukan dengan menggunakan $injector layanan ini adalah memecahkan masalah ayam-dan-telur dengan menunda ketergantungan $ http pada AuthService.

Saya percaya bahwa apa yang Anda lakukan sebenarnya adalah cara paling sederhana untuk melakukannya.

Anda juga bisa melakukan ini dengan:

  • Mendaftarkan pencegat nanti (melakukannya di run()blok, bukan di config()blok, mungkin sudah berhasil). Tetapi dapatkah Anda menjamin bahwa $ http belum dipanggil?
  • "Memasukkan" $ http secara manual ke AuthService saat Anda mendaftarkan pencegat dengan menelepon AuthService.setHttp() atau sesuatu.
  • ...

15
Bagaimana jawaban ini memecahkan masalah, saya tidak mengerti? @shaunlim
In Gumus

1
Sebenarnya itu tidak menyelesaikannya, itu hanya menunjukkan bahwa aliran algoritma itu buruk.
Roman M. Koss

12
Anda tidak dapat mendaftarkan pencegat dalam sebuah run()blok, karena Anda tidak dapat menginjeksi $ httpProvider ke blok yang dijalankan. Anda hanya dapat melakukannya di fase konfigurasi.
Stephen Friedrich

2
Poin bagus adalah referensi melingkar, tetapi sebaliknya itu tidak boleh menjadi jawaban yang diterima. Tak satu pun dari poin-poin yang masuk akal
Nikolai

65

Inilah yang akhirnya saya lakukan

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

Catatan: $injector.getPanggilan harus berada dalam metode interseptor, jika Anda mencoba menggunakannya di tempat lain, Anda akan terus mendapatkan kesalahan dependensi melingkar di JS.


4
Menggunakan injeksi manual ($ injector.get ('Auth')) memecahkan masalah. Kerja bagus!
Robert

Untuk menghindari ketergantungan melingkar, saya memeriksa url mana yang dipanggil. if (! config.url.includes ('/ oauth / v2 / token') && config.url.includes ('/ api')) {// Panggil OAuth Service}. Oleh karena itu tidak ada lagi ketergantungan melingkar. Setidaknya untuk saya sendiri itu berhasil;).
Brieuc

Sempurna. Inilah yang saya butuhkan untuk memperbaiki masalah serupa. Terima kasih @shaunlim!
Martyn Chamberlin

Saya tidak terlalu menyukai solusi ini karena layanan ini anonim dan tidak mudah ditangani untuk pengujian. Solusi yang jauh lebih baik untuk diinjeksi saat runtime.
kmanzana

Itu berhasil untuk saya. Pada dasarnya menginjeksi layanan yang menggunakan $ http.
Thomas

15

Saya pikir menggunakan $ injektor secara langsung adalah antipattern.

Cara untuk memutus dependensi melingkar adalah dengan menggunakan peristiwa: Daripada memasukkan $ state, injeksikan $ rootScope. Alih-alih mengarahkan secara langsung, lakukan

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

plus

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

2
Saya pikir ini adalah solusi yang lebih elegan, karena tidak akan memiliki ketergantungan, kami juga dapat mendengarkan acara ini di banyak tempat di mana pun relevan
Basav

Ini tidak akan berfungsi untuk kebutuhan saya, karena saya tidak dapat memiliki nilai pengembalian setelah mengirim acara.
xabitrigo

13

Logika buruk membuat hasil seperti itu

Sebenarnya tidak ada gunanya mencari apakah pengguna ditulis atau tidak di Http Interceptor. Saya akan merekomendasikan untuk membungkus semua permintaan HTTP Anda ke dalam satu .service (atau .factory, atau ke .provider), dan menggunakannya untuk SEMUA permintaan. Pada setiap kali Anda memanggil fungsi, Anda dapat memeriksa apakah pengguna masuk atau tidak. Jika semuanya baik-baik saja, izinkan kirim permintaan.

Dalam kasus Anda, aplikasi Angular akan mengirimkan permintaan dalam hal apa pun, Anda hanya memeriksa otorisasi di sana, dan setelah itu JavaScript akan mengirim permintaan.

Inti dari masalah Anda

myHttpInterceptordisebut di bawah $httpProvidercontoh. AuthServicePenggunaan Anda $http, atau $resource, dan di sini Anda memiliki rekursi ketergantungan, atau ketergantungan melingkar. Jika Anda menghapus ketergantungan itu dari AuthService, Anda tidak akan melihat kesalahan itu.


Juga seperti yang ditunjukkan oleh @Pieter Herroelen, Anda dapat menempatkan pencegat ini di modul Anda module.run, tetapi ini lebih seperti peretasan, bukan solusi.

Jika Anda ingin melakukan kode deskriptif dan bersih, Anda harus menggunakan beberapa prinsip SOLID.

Setidaknya prinsip Single Responsibility akan banyak membantu Anda dalam situasi seperti itu.


5
Saya tidak berpikir jawaban ini baik worded, tapi saya lakukan pikir itu sampai ke akar masalah. Masalah dengan layanan auth yang menyimpan data pengguna saat ini dan cara masuk (permintaan http) adalah bahwa ia bertanggung jawab atas dua hal. Jika itu dibagi menjadi satu layanan untuk menyimpan data pengguna saat ini, dan layanan lain untuk masuk, maka penyadapan http hanya perlu bergantung pada "layanan pengguna saat ini", dan tidak lagi membuat ketergantungan melingkar.
Snixtor

@Snixtor Terima kasih! Saya perlu belajar bahasa Inggris lebih banyak, agar lebih jelas.
Roman M. Koss

0

Jika Anda hanya memeriksa status Auth (isAuthorized ()), saya akan merekomendasikan untuk meletakkan status tersebut dalam modul terpisah, katakan "Auth", yang hanya menampung status dan tidak menggunakan $ http itu sendiri.

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

Modul Auth:

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
}

(saya menggunakan localStorage untuk menyimpan sessionId di sisi klien di sini, tetapi Anda juga dapat mengatur ini di dalam AuthService Anda setelah panggilan $ http misalnya)

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.