Angular аутентификация входа в систему ui-router

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

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

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

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

Ответ 1

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

Взгляните на этот плунжер.

Сначала вам нужна служба для хранения идентификатора пользователя. Я называю это 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;
      }
    };
  }
])

Во-вторых, вам нужна служба, которая проверяет состояние, к которому пользователь хочет перейти, убедитесь, что они вошли в систему (при необходимости, не обязательно для signin, password reset и т.д.), а затем выполняет роль проверьте (если ваше приложение нуждается в этом). Если они не аутентифицированы, отправьте их на страницу входа. Если они аутентифицированы, но не выполняют проверку роли, отправьте их на страницу с запретом доступа. Я вызываю эту службу 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 $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, если разрешение было выполнено, которое охватывает любое время вы перемещаетесь по состояниям.

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

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

Куда мы идем дальше? Ну, вы можете организовать свои штаты в регионах, для которых требуется вход. Вы можете потребовать аутентифицированных/авторизованных пользователей, добавив 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>

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

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

Ответ 2

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

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

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

Вы можете слушать $locationChangeSuccess, и вы можете выполнить некоторую логику, даже асинхронную логику. Исходя из этой логики, вы можете позволить функции возвращать undefined, что приведет к продолжению перехода состояния как обычно, или вы можете сделать $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()
      }
    }
  }
)

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

Ответ 3

Самое простое решение - использовать $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');
        }
      });
    }]
  );

Ответ 4

Мне кажется, вам нужен service, который обрабатывает процесс аутентификации (и его хранилище).

В этой службе вам понадобятся основные методы:

  • isAuthenticated()
  • login()
  • logout()
  • и т.д.

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

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

Хорошим и надежным примером такого поведения является проект angular-app и, в частности, его модуль защиты, который основан на awesome HTTP Auth Interceptor Module

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

Ответ 5

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

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

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

Или также

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

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

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

Ответ 6

Я хотел поделиться другим решением, работающим с 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 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, я использую переходы для проверки состояния, прошедшего проверку.

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

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

Надеюсь, это может помочь кому угодно.

Ответ 7

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

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

Ответ 8

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

$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 */ }

Ответ 9

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

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

Наконец, вам нужно каким-то образом определить, может ли ваш пользователь в настоящий момент выполнять определенные операции. Этого можно достичь, добавив функцию "can" к вашей службе auth. Может принимать два параметра:  - действие - требуется - (т.е. "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');
      }
   }
}]

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

Ответ 10

Использовать перехватчик $http

С помощью перехватчика $http вы можете отправлять заголовки в конец или наоборот и выполнять свои проверки таким образом.

Отличная статья о $http перехватчики

Пример:

$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 функцию.