it-roy-ru.com

Аутентификация в AngularJS для пользовательского интерфейса маршрутизатора

Я новичок в AngularJS, и меня немного смущает, как я могу использовать angular- "ui-router" в следующем сценарии:

Я строю веб-приложение, которое состоит из двух разделов. Первый раздел - это домашняя страница с ее представлениями для входа и регистрации, а второй раздел - это панель управления (после успешного входа в систему).

Я создал index.html для домашнего раздела с его приложением angular и ​​конфигурацией ui-router для обработки представлений /login и /signup, и есть еще один файл dashboard.html для раздела панели управления с его приложением и конфигурацией ui-router для обработки многих вложенных представлений ,.

Теперь я закончил раздел панели инструментов и не знаю, как объединить эти два раздела с их различными приложениями angular. Как я могу сказать домашнему приложению перенаправить приложение на панель инструментов?

371
Ahmed Hashem

Я нахожусь в процессе создания хорошей демонстрации, а также превращения некоторых из этих сервисов в полезный модуль, но вот что я придумала. Это сложный процесс для обхода некоторых предостережений, так что оставайтесь там. Вам нужно будет разбить это на несколько частей.

Посмотрите на этот план .

Во-первых, вам нужен сервис для хранения личности пользователя. Я называю это principal. Это может быть проверено, чтобы видеть, вошел ли пользователь в систему, и по запросу, это может разрешить объект, который представляет важную информацию о личности пользователя. Это может быть все, что вам нужно, но основными являются отображаемое имя, имя пользователя, возможно, адрес электронной почты и роли, к которым принадлежит пользователь (если это относится к вашему приложению). У принципала также есть методы для проверки ролей.

.factory('principal', ['$q', '$http', '$timeout',
  function($q, $http, $timeout) {
    var _identity = undefined,
      _authenticated = false;

    return {
      isIdentityResolved: function() {
        return angular.isDefined(_identity);
      },
      isAuthenticated: function() {
        return _authenticated;
      },
      isInRole: function(role) {
        if (!_authenticated || !_identity.roles) return false;

        return _identity.roles.indexOf(role) != -1;
      },
      isInAnyRole: function(roles) {
        if (!_authenticated || !_identity.roles) return false;

        for (var i = 0; i < roles.length; i++) {
          if (this.isInRole(roles[i])) return true;
        }

        return false;
      },
      authenticate: function(identity) {
        _identity = identity;
        _authenticated = identity != null;
      },
      identity: function(force) {
        var deferred = $q.defer();

        if (force === true) _identity = undefined;

        // check and see if we have retrieved the 
        // identity data from the server. if we have, 
        // reuse it by immediately resolving
        if (angular.isDefined(_identity)) {
          deferred.resolve(_identity);

          return deferred.promise;
        }

        // otherwise, retrieve the identity data from the
        // server, update the identity object, and then 
        // resolve.
        //           $http.get('/svc/account/identity', 
        //                     { ignoreErrors: true })
        //                .success(function(data) {
        //                    _identity = data;
        //                    _authenticated = true;
        //                    deferred.resolve(_identity);
        //                })
        //                .error(function () {
        //                    _identity = null;
        //                    _authenticated = false;
        //                    deferred.resolve(_identity);
        //                });

        // for the sake of the demo, fake the lookup
        // by using a timeout to create a valid
        // fake identity. in reality,  you'll want 
        // something more like the $http request
        // commented out above. in this example, we fake 
        // looking up to find the user is
        // not logged in
        var self = this;
        $timeout(function() {
          self.authenticate(null);
          deferred.resolve(_identity);
        }, 1000);

        return deferred.promise;
      }
    };
  }
])

Во-вторых, вам нужна служба, которая проверяет состояние, в которое пользователь хочет перейти, проверяет, вошли ли они в систему (при необходимости; не требуется для входа, сброса пароля и т.д.), А затем выполняет проверку роли (если ваше приложение). нужно это). Если они не аутентифицированы, отправьте их на страницу входа. Если они прошли проверку подлинности, но не прошли проверку роли, отправьте их на страницу отказа в доступе. Я называю эту услугу authorization.

.factory('authorization', ['$rootScope', '$state', 'principal',
  function($rootScope, $state, principal) {
    return {
      authorize: function() {
        return principal.identity()
          .then(function() {
            var isAuthenticated = principal.isAuthenticated();

            if ($rootScope.toState.data.roles
                && $rootScope.toState
                             .data.roles.length > 0 
                && !principal.isInAnyRole(
                   $rootScope.toState.data.roles))
            {
              if (isAuthenticated) {
                  // user is signed in but not
                  // authorized for desired state
                  $state.go('accessdenied');
              } else {
                // user is not authenticated. Stow
                // the state they wanted before you
                // send them to the sign-in state, so
                // you can return them when you're done
                $rootScope.returnToState
                    = $rootScope.toState;
                $rootScope.returnToStateParams
                    = $rootScope.toStateParams;

                // now, send them to the signin state
                // so they can log in
                $state.go('signin');
              }
            }
          });
      }
    };
  }
])

Теперь все, что вам нужно сделать, это прослушать ui-router 's $stateChangeStart . Это дает вам возможность проверить текущее состояние, состояние, в которое они хотят перейти, и вставить вашу проверку авторизации. Если это не удается, вы можете отменить переход по маршруту или перейти на другой маршрут.

.run(['$rootScope', '$state', '$stateParams', 
      'authorization', 'principal',
    function($rootScope, $state, $stateParams, 
             authorization, principal)
{
      $rootScope.$on('$stateChangeStart', 
          function(event, toState, toStateParams)
      {
        // track the state the user wants to go to; 
        // authorization service needs this
        $rootScope.toState = toState;
        $rootScope.toStateParams = toStateParams;
        // if the principal is resolved, do an 
        // authorization check immediately. otherwise,
        // it'll be done when the state it resolved.
        if (principal.isIdentityResolved()) 
            authorization.authorize();
      });
    }
  ]);

Сложная задача по отслеживанию личности пользователя - поиск, если вы уже прошли аутентификацию (например, вы посещаете страницу после предыдущего сеанса и сохранили токен аутентификации в файле cookie, или, может быть, вы сильно обновили страницу, или упал на URL из ссылки). Из-за того, как работает ui-router, вам нужно выполнить одно и то же определение личности перед проверкой аутентификации. Вы можете сделать это, используя опцию resolve в вашей конфигурации состояния. У меня есть одно родительское состояние для сайта, от которого наследуются все состояния, что заставляет принципал быть разрешенным прежде, чем что-либо еще произойдет.

$stateProvider.state('site', {
  'abstract': true,
  resolve: {
    authorize: ['authorization',
      function(authorization) {
        return authorization.authorize();
      }
    ]
  },
  template: '<div ui-view />'
})

Здесь есть еще одна проблема ... resolve вызывается только один раз. Как только ваше обещание поиска личности будет выполнено, он больше не будет запускать делегат разрешения. Поэтому мы должны выполнить ваши проверки подлинности в двух местах: один раз в соответствии с вашим обещанием идентификации, разрешенным в resolve, которое покрывает первую загрузку вашего приложения, и один раз в $stateChangeStart, если разрешение было выполнено, которое покрывает любое время, когда вы перемещаетесь по штатам. ,.

Хорошо, так что мы сделали до сих пор?

  1. Мы проверяем, когда приложение загружается, если пользователь вошел в систему.
  2. Мы отслеживаем информацию о зарегистрированном пользователе.
  3. Мы перенаправляем их в состояние входа для состояний, требующих входа пользователя.
  4. Мы перенаправляем их в состояние отказа в доступе, если у них нет прав доступа к нему.
  5. У нас есть механизм для перенаправления пользователей обратно в исходное состояние, которое они запрашивали, если нам нужно, чтобы они вошли в систему.
  6. Мы можем вывести пользователя из системы (необходимо подключить его к любому клиентскому или серверному коду, который управляет вашим авторизационным билетом).
  7. Нам не нужно отправлять пользователей обратно на страницу входа каждый раз, когда они перезагружают свой браузер или переходят по ссылке.

Куда мы отправимся отсюда? Ну, вы можете организовать свои состояния в регионы, требующие входа. Вы можете требовать аутентифицированных/авторизованных пользователей, добавив data с roles к этим состояниям (или к их родителям, если вы хотите использовать наследование). Здесь мы ограничиваем ресурс для администраторов:

.state('restricted', {
    parent: 'site',
    url: '/restricted',
    data: {
      roles: ['Admin']
    },
    views: {
      '[email protected]': {
        templateUrl: 'restricted.html'
      }
    }
  })

Теперь вы можете контролировать состояние за состоянием, что пользователи могут получить доступ к маршруту. Любые другие проблемы? Может быть, меняется только часть представления в зависимости от того, вошли они или нет? Нет проблем. Используйте principal.isAuthenticated() или даже principal.isInRole() с любым из многочисленных способов условного отображения шаблона или элемента.

Сначала вставьте principal в контроллер или что-то еще, и прикрепите его к области видимости, чтобы вы могли легко использовать его в своем представлении:

.scope('HomeCtrl', ['$scope', 'principal', 
    function($scope, principal)
{
  $scope.principal = principal;
});

Показать или скрыть элемент:

<div ng-show="principal.isAuthenticated()">
   I'm logged in
</div>
<div ng-hide="principal.isAuthenticated()">
  I'm not logged in
</div>

И так далее, и так далее, и тому подобное. В любом случае, в вашем примере приложения у вас будет состояние для домашней страницы, которое позволит неаутентифицированным пользователям заходить. У них могут быть ссылки на состояния входа или регистрации или встроенные формы на этой странице. Все, что тебе подходит.

Страницы панели мониторинга могут наследовать состояние, требующее входа пользователей в систему и, скажем, член роли User. Все авторизационные материалы, которые мы обсуждали, будут исходить оттуда.

604
HackedByChinese

Решения, опубликованные до сих пор, на мой взгляд, излишне сложны. Есть более простой способ. документация по ui-router говорит, что прослушивает $locationChangeSuccess и использует $urlRouter.sync() для проверки перехода состояния, его остановки или возобновления. Но даже это на самом деле не работает.

Однако вот две простые альтернативы. Выбери один:

Решение 1: прослушивание $locationChangeSuccess

Вы можете слушать $locationChangeSuccess и выполнять некоторую логику, даже асинхронную логику. Основываясь на этой логике, вы можете позволить функции возвращать неопределенное значение, что приведет к тому, что переход состояния будет продолжаться как обычно, или вы можете выполнить $state.go('logInPage'), если пользователю требуется аутентификация. Вот пример:

angular.module('App', ['ui.router'])

// In the run phase of your Angular application  
.run(function($rootScope, user, $state) {

  // Listen to '$locationChangeSuccess', not '$stateChangeStart'
  $rootScope.$on('$locationChangeSuccess', function() {
    user
      .logIn()
      .catch(function() {
        // log-in promise failed. Redirect to log-in page.
        $state.go('logInPage')
      })
  })
})

Имейте в виду, что это на самом деле не препятствует загрузке целевого состояния, но оно перенаправляет на страницу входа в систему, если пользователь не авторизован. Это нормально, так как настоящая защита на сервере, в любом случае.

Решение 2: использование состояния resolve

В этом решении вы используете функция разрешения ui-router .

Вы в основном отклоняете обещание в resolve, если пользователь не прошел проверку подлинности, а затем перенаправляете его на страницу входа.

Вот как это происходит:

angular.module('App', ['ui.router'])

.config(
  function($stateProvider) {
    $stateProvider
      .state('logInPage', {
        url: '/logInPage',
        templateUrl: 'sections/logInPage.html',
        controller: 'logInPageCtrl',
      })
      .state('myProtectedContent', {
        url: '/myProtectedContent',
        templateUrl: 'sections/myProtectedContent.html',
        controller: 'myProtectedContentCtrl',
        resolve: { authenticate: authenticate }
      })
      .state('alsoProtectedContent', {
        url: '/alsoProtectedContent',
        templateUrl: 'sections/alsoProtectedContent.html',
        controller: 'alsoProtectedContentCtrl',
        resolve: { authenticate: authenticate }
      })

    function authenticate($q, user, $state, $timeout) {
      if (user.isAuthenticated()) {
        // Resolve the promise successfully
        return $q.when()
      } else {
        // The next bit of code is asynchronously tricky.

        $timeout(function() {
          // This code runs after the authentication promise has been rejected.
          // Go to the log-in page
          $state.go('logInPage')
        })

        // Reject the authentication promise to prevent the state from loading
        return $q.reject()
      }
    }
  }
)

В отличие от первого решения, это решение фактически предотвращает загрузку целевого состояния.

118
M.K. Safi

Самое простое решение - использовать $stateChangeStart и event.preventDefault(), чтобы отменить изменение состояния, когда пользователь не аутентифицирован, и перенаправить его в состояние auth, которое является страницей входа в систему.

angular
  .module('myApp', [
    'ui.router',
  ])
    .run(['$rootScope', 'User', '$state',
    function ($rootScope, User, $state) {
      $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
        if (toState.name !== 'auth' && !User.authenticaded()) {
          event.preventDefault();
          $state.go('auth');
        }
      });
    }]
  );
42
sebest

Я думаю, вам нужно service, который обрабатывает процесс аутентификации (и его хранение).

В этом сервисе вам понадобятся несколько основных методов:

  • isAuthenticated()
  • login()
  • logout()
  • так далее ...

Эта услуга должна быть внедрена в ваши контроллеры каждого модуля:

  • В разделе панели инструментов используйте этот сервис, чтобы проверить, прошел ли аутентификация пользователя (метод service.isAuthenticated()). если нет, перенаправить на/логин
  • В разделе входа в систему просто используйте данные формы для аутентификации пользователя с помощью вашего метода service.login()

Хорошим и надежным примером такого поведения является проект angular-app и, в частности, его модуль безопасности , основанный на потрясающем модуль HTTP-аутентификации-перехватчика

Надеюсь это поможет

22
Cétia

Я создал этот модуль, чтобы помочь сделать этот процесс кусок пирога

Вы можете делать такие вещи, как:

$routeProvider
  .state('secret',
    {
      ...
      permissions: {
        only: ['admin', 'god']
      }
    });

Или также

$routeProvider
  .state('userpanel',
    {
      ...
      permissions: {
        except: ['not-logged-in']
      }
    });

Это совершенно новый, но стоит проверить!

https://github.com/Narzerus/angular-permission

21
Rafael Vidaurre

Я хотел бы поделиться другим решением, работающим с UI Router 1.0.0.X

Как вы, возможно, знаете, stateChangeStart и stateChangeSuccess теперь устарели. https://github.com/angular-ui/ui-router/issues/2655

Вместо этого вы должны использовать $ transitions http://angular-ui.github.io/ui-router/1.0.0-alpha.1/interfaces/transition.ihookregistry.html

Вот как я этого добился:

Сначала у меня есть и AuthService с некоторыми полезными функциями

angular.module('myApp')

        .factory('AuthService',
                ['$http', '$cookies', '$rootScope',
                    function ($http, $cookies, $rootScope) {
                        var service = {};

                        // Authenticates throug a rest service
                        service.authenticate = function (username, password, callback) {

                            $http.post('api/login', {username: username, password: password})
                                    .success(function (response) {
                                        callback(response);
                                    });
                        };

                        // Creates a cookie and set the Authorization header
                        service.setCredentials = function (response) {
                            $rootScope.globals = response.token;

                            $http.defaults.headers.common['Authorization'] = 'Bearer ' + response.token;
                            $cookies.put('globals', $rootScope.globals);
                        };

                        // Checks if it's authenticated
                        service.isAuthenticated = function() {
                            return !($cookies.get('globals') === undefined);
                        };

                        // Clear credentials when logout
                        service.clearCredentials = function () {
                            $rootScope.globals = undefined;
                            $cookies.remove('globals');
                            $http.defaults.headers.common.Authorization = 'Bearer ';
                        };

                        return service;
                    }]);

Тогда у меня есть эта конфигурация:

angular.module('myApp', [
    'ui.router',
    'ngCookies'
])
        .config(['$stateProvider', '$urlRouterProvider',
            function ($stateProvider, $urlRouterProvider) {
                $urlRouterProvider.otherwise('/resumen');
                $stateProvider
                        .state("dashboard", {
                            url: "/dashboard",
                            templateUrl: "partials/dashboard.html",
                            controller: "dashCtrl",
                            data: {
                                authRequired: true
                            }
                        })
                        .state("login", {
                            url: "/login",
                            templateUrl: "partials/login.html",
                            controller: "loginController"
                        })
            }])

        .run(['$rootScope', '$transitions', '$state', '$cookies', '$http', 'AuthService',
            function ($rootScope, $transitions, $state, $cookies, $http, AuthService) {

                // keep user logged in after page refresh
                $rootScope.globals = $cookies.get('globals') || {};
                $http.defaults.headers.common['Authorization'] = 'Bearer ' + $rootScope.globals;

                $transitions.onStart({
                    to: function (state) {
                        return state.data != null && state.data.authRequired === true;
                    }
                }, function () {
                    if (!AuthService.isAuthenticated()) {
                        return $state.target("login");
                    }
                });
            }]);

Вы можете видеть, что я использую

data: {
   authRequired: true
}

отмечать состояние доступно только если аутентифицирован.

затем на . run я использую переходы, чтобы проверить состояние autheated

$transitions.onStart({
    to: function (state) {
        return state.data != null && state.data.authRequired === true;
    }
}, function () {
    if (!AuthService.isAuthenticated()) {
        return $state.target("login");
    }
});

Я построил этот пример, используя некоторый код, найденный в документации $ transitions. Я довольно новый пользовательский интерфейс роутера, но он работает.

Надеюсь, что это может помочь любому.

14
Sergio Fernandez

Вот как мы вышли из бесконечного цикла маршрутизации и по-прежнему использовали $state.go вместо $location.path

if('401' !== toState.name) {
  if (principal.isIdentityResolved()) authorization.authorize();
}
5
Jason Girdner

У меня есть другое решение: это решение прекрасно работает, когда у вас есть только контент, который вы хотите показать при входе в систему. Определите правило, в котором вы проверяете, вошли ли вы в систему, а не путь к маршрутам из белого списка.

$urlRouterProvider.rule(function ($injector, $location) {
   var UserService = $injector.get('UserService');
   var path = $location.path(), normalized = path.toLowerCase();

   if (!UserService.isLoggedIn() && path.indexOf('login') === -1) {
     $location.path('/login/signin');
   }
});

В моем примере я спрашиваю, не вошел ли я в систему, и текущий маршрут, который я хочу направить, не является частью `/ login ', потому что мои маршруты из белого списка следующие

/login/signup // registering new user
/login/signin // login to app

поэтому у меня есть мгновенный доступ к этим двум маршрутам, и каждый другой маршрут будет проверен, если вы находитесь в сети.

Вот весь мой файл маршрутизации для модуля входа

export default (
  $stateProvider,
  $locationProvider,
  $urlRouterProvider
) => {

  $stateProvider.state('login', {
    parent: 'app',
    url: '/login',
    abstract: true,
    template: '<ui-view></ui-view>'
  })

  $stateProvider.state('signin', {
    parent: 'login',
    url: '/signin',
    template: '<login-signin-directive></login-signin-directive>'
  });

  $stateProvider.state('lock', {
    parent: 'login',
    url: '/lock',
    template: '<login-lock-directive></login-lock-directive>'
  });

  $stateProvider.state('signup', {
    parent: 'login',
    url: '/signup',
    template: '<login-signup-directive></login-signup-directive>'
  });

  $urlRouterProvider.rule(function ($injector, $location) {
    var UserService = $injector.get('UserService');
    var path = $location.path();

    if (!UserService.isLoggedIn() && path.indexOf('login') === -1) {
         $location.path('/login/signin');
    }
  });

  $urlRouterProvider.otherwise('/error/not-found');
}

() => { /* code */ } - это синтаксис ES6, используйте вместо него function() { /* code */ }

2
Chris Incoqnito

Сначала вам понадобится сервис, который вы можете внедрить в свои контроллеры, который имеет некоторое представление о состоянии аутентификации приложения. Сохранение деталей аутентификации с локальным хранилищем - достойный способ приблизиться к этому.

Далее вам нужно проверить состояние аутентификации непосредственно перед изменением состояния. Поскольку в вашем приложении есть некоторые страницы, которые должны быть аутентифицированы, а другие нет, создайте родительский маршрут, который проверяет аутентификацию, и сделайте все остальные страницы, которые требуют того же самого, дочерними по отношению к этому родителю.

Наконец, вам понадобится какой-то способ узнать, может ли ваш вошедший в систему пользователь выполнить определенные операции. Этого можно достичь, добавив функцию "can" в службу аутентификации. Может принимать два параметра: - действие - требуется - (т. Е. 'Manage_dashboards' или 'create_new_dashboard') - объект - необязательно - объект, над которым выполняется операция. Например, если у вас есть объект панели мониторинга, вы можете проверить, есть ли dashboard.ownerId === loggedInUser.id. (Конечно, информация, передаваемая от клиента, никогда не должна быть доверенной, и вы всегда должны проверять это на сервере, прежде чем записывать ее в свою базу данных).

angular.module('myApp', ['ngStorage']).config([
   '$stateProvider',
function(
   $stateProvider
) {
   $stateProvider
     .state('home', {...}) //not authed
     .state('sign-up', {...}) //not authed
     .state('login', {...}) //not authed
     .state('authed', {...}) //authed, make all authed states children
     .state('authed.dashboard', {...})
}])
.service('context', [
   '$localStorage',
function(
   $localStorage
) {
   var _user = $localStorage.get('user');
   return {
      getUser: function() {
         return _user;
      },
      authed: function() {
         return (_user !== null);
      },
      // server should return some kind of token so the app 
      // can continue to load authenticated content without having to
      // re-authenticate each time
      login: function() {
         return $http.post('/login.json').then(function(reply) {
            if (reply.authenticated === true) {
               $localStorage.set(_userKey, reply.user);
            }
         });
      },
      // this request should expire that token, rendering it useless
      // for requests outside of this session
      logout: function() {
         return $http.post('logout.json').then(function(reply) {
            if (reply.authenticated === true) {
               $localStorage.set(_userKey, reply.user);
            }
         });
      },
      can: function(action, object) {
         if (!this.authed()) {
            return false;
         }

         var user = this.getUser();

         if (user && user.type === 'admin') {
             return true;
         }

         switch(action) {
            case 'manage_dashboards':
               return (user.type === 'manager');
         }

         return false;


      }
   }
}])
.controller('AuthCtrl', [
   'context', 
   '$scope', 
function(
   context, 
   $scope
) {
   $scope.$root.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
      //only require auth if we're moving to another authed page
      if (toState && toState.name.indexOf('authed') > -1) {
         requireAuth();
      }
   });

   function requireAuth() {
      if (!context.authed()) {
         $state.go('login');
      }
   }
}]

** ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: приведенный выше код является псевдокодом и не дает никаких гарантий **

2
colefner

Используйте $ http Interceptor

Используя перехватчик $ http, вы можете отправлять заголовки в Back-end или наоборот и выполнять свои проверки таким образом.

Отличная статья о $ http interceptors

Пример:

$httpProvider.interceptors.Push(function ($q) {
        return {
            'response': function (response) {

                // TODO Create check for user authentication. With every request send "headers" or do some other check
                return response;
            },
            'responseError': function (reject) {

                // Forbidden
                if(reject.status == 403) {
                    console.log('This page is forbidden.');
                    window.location = '/';
                // Unauthorized
                } else if(reject.status == 401) {
                    console.log("You're not authorized to view this page.");
                    window.location = '/';
                }

                return $q.reject(reject);
            }
        };
    });

Поместите это в вашу функцию .config или .run.

2
TSlegaitis