web-dev-qa-db-fra.com

Authentification de connexion AngularJS ui-routeur

Je suis nouveau dans AngularJS et je suis un peu confus quant à la façon dont je peux utiliser angular- "ui-router" dans le scénario suivant:

Je construis une application Web qui se compose de deux sections. La première section est la page d'accueil avec ses vues de connexion et d'inscription, et la deuxième section est le tableau de bord (après une connexion réussie).

J'ai créé un index.html pour la section home avec son application angular et ui-router config pour gérer /login et /signup, et il existe un autre fichier. dashboard.html pour la section du tableau de bord avec son application et ui-router config pour gérer de nombreuses sous-vues.

Maintenant, j'ai terminé la section du tableau de bord et je ne sais pas comment combiner les deux sections avec leurs différentes applications angular. Comment pourrais-je dire à l'application d'accueil de rediriger vers l'application du tableau de bord?

372
Ahmed Hashem

Je suis en train de faire une démo plus agréable et de nettoyer certains de ces services pour en faire un module utilisable, mais voici ce que j'ai proposé. Il s'agit d'un processus complexe pour contourner certaines mises en garde, alors accrochez-vous. Vous aurez besoin de le décomposer en plusieurs morceaux.

Regardez ce plunk .

Tout d'abord, vous avez besoin d'un service pour stocker l'identité de l'utilisateur. J'appelle cela principal. Vous pouvez vérifier si l'utilisateur est connecté et, sur demande, résoudre un objet représentant les informations essentielles sur l'identité de l'utilisateur. Cela peut être ce dont vous avez besoin, mais l'essentiel serait un nom d'affichage, un nom d'utilisateur, éventuellement un courrier électronique, ainsi que les rôles auxquels un utilisateur appartient (si cela s'applique à votre application). Principal dispose également de méthodes pour effectuer des contrôles de rôle.

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

Deuxièmement, vous avez besoin d’un service qui vérifie l’état auquel l’utilisateur souhaite accéder, s’assure qu’il est connecté (si nécessaire; inutile pour la connexion, la réinitialisation du mot de passe, etc.), puis effectue une vérification des rôles (si votre application est activée). a besoin de cela). S'ils ne sont pas authentifiés, envoyez-les à la page de connexion. S'ils sont authentifiés mais échouent à la vérification du rôle, envoyez-les à une page d'accès refusé. J'appelle ce service 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');
              }
            }
          });
      }
    };
  }
])
_

Il ne vous reste plus qu'à écouter les _ui-router_ $stateChangeStart . Cela vous donne l'occasion d'examiner l'état actuel, l'état dans lequel ils souhaitent aller et d'insérer votre contrôle d'autorisation. Si cela échoue, vous pouvez annuler la transition d’itinéraire ou changer d’itinéraire.

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

La partie délicate du suivi de l’identité d’un utilisateur consiste à le rechercher si vous vous êtes déjà authentifié (par exemple, si vous visitez la page après une session précédente et que vous avez enregistré un jeton d’authentification dans un cookie, ou si vous avez actualisé une page, ou déposé sur une URL depuis un lien). En raison du fonctionnement de _ui-router_, vous devez procéder à la résolution de votre identité une fois, avant votre vérification d'authentification. Vous pouvez le faire en utilisant l'option resolve dans votre configuration d'état. J'ai un état parent pour le site dont tous les états héritent, ce qui oblige le principal à être résolu avant toute autre situation.

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

Il y a un autre problème ici ... resolve n'est appelé qu'une fois. Une fois que votre promesse de recherche d'identité est terminée, le délégué de résolution ne sera plus exécuté. Nous devons donc effectuer vos vérifications d’authentification à deux endroits: une fois conformément à votre promesse d’identité, résolue dans resolve, qui couvre le premier chargement de votre application, et une fois dans _$stateChangeStart_ si la résolution a été effectuée, qui couvre chaque fois que vous naviguez dans les États.

OK, qu'avons-nous fait jusqu'à présent?

  1. Nous vérifions si l'application se charge si l'utilisateur est connecté.
  2. Nous suivons les informations sur l'utilisateur connecté.
  3. Nous les redirigeons vers l'état de connexion pour les états qui nécessitent la connexion de l'utilisateur.
  4. Nous les redirigeons vers un état d'accès refusé s'ils ne sont pas autorisés à y accéder.
  5. Nous disposons d'un mécanisme pour rediriger les utilisateurs vers l'état d'origine qu'ils ont demandé, si nous avions besoin d'eux pour se connecter.
  6. Nous pouvons déconnecter un utilisateur (il doit être connecté à tout code de client ou de serveur qui gère votre ticket d'authentification).
  7. Nous n'avons pas besoin de renvoyer les utilisateurs à la page de connexion chaque fois qu'ils rechargent leur navigateur ou insèrent un lien.

Où allons-nous à partir d'ici? Eh bien, vous pouvez organiser vos états en régions nécessitant une connexion. Vous pouvez exiger des utilisateurs authentifiés/autorisés en ajoutant data avec roles à ces états (ou à un de leurs parents, si vous souhaitez utiliser l'héritage ). Ici, nous limitons une ressource aux administrateurs:

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

Vous pouvez maintenant contrôler, état par état, quels utilisateurs peuvent accéder à un itinéraire. D'autres préoccupations? Peut-être que vous ne modifiez qu'une partie d'une vue en fonction de leur connexion ou non? Aucun problème. Utilisez la principal.isAuthenticated() ou même principal.isInRole() avec l’un des nombreux moyens d’afficher de manière conditionnelle un modèle ou un élément.

Commencez par injecter principal dans un contrôleur ou autre, puis collez-le sur l’oscilloscope pour pouvoir l’utiliser facilement à vos yeux:

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

Afficher ou masquer un élément:

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

Etc., etc., etc. De toute façon, dans votre exemple d'application, vous auriez un état pour la page d'accueil qui laisserait passer des utilisateurs non authentifiés. Ils pourraient avoir des liens vers les états de connexion ou d'inscription, ou intégrer ces formulaires dans cette page. Tout ce qui vous convient.

Les pages du tableau de bord peuvent toutes hériter d'un état qui requiert que les utilisateurs soient connectés et, par exemple, être un membre du rôle User. Toutes les autorisations dont nous avons parlé découleraient de là.

603
HackedByChinese

Les solutions affichées jusqu'ici sont inutilement compliquées, à mon avis. Il y a un moyen plus simple. Le documentation de ui-router indique d'écouter $locationChangeSuccess et d'utiliser $urlRouter.sync() pour vérifier une transition d'état, l'arrêter ou la reprendre. Mais même cela ne fonctionne pas réellement.

Cependant, voici deux alternatives simples. Choisissez-en un:

Solution 1: écouter sur $locationChangeSuccess

Vous pouvez écouter $locationChangeSuccess et vous pouvez y exécuter une logique, même asynchrone. Sur la base de cette logique, vous pouvez laisser la fonction retourner non définie, ce qui entraînera la transition d'état comme d'habitude, ou bien vous pouvez utiliser $state.go('logInPage') si l'utilisateur doit être authentifié. Voici un exemple:

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

N'oubliez pas que cela n'empêche pas le chargement de l'état cible, mais qu'il redirige vers la page de connexion si l'utilisateur n'est pas autorisé. Ce n'est pas grave, car la véritable protection est sur le serveur.

Solution 2: utilisation de l'état resolve

Dans cette solution, vous utilisez ui-router fonctionnalité de résolution .

En gros, vous refusez la promesse dans resolve si l'utilisateur n'est pas authentifié, puis vous les redirigez vers la page de connexion.

Voici comment ça se passe:

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()
      }
    }
  }
)

Contrairement à la première solution, cette solution empêche en réalité le chargement de l'état cible.

118
M.K. Safi

La solution la plus simple consiste à utiliser $stateChangeStart et event.preventDefault() pour annuler le changement d'état lorsque l'utilisateur n'est pas authentifié et le rediriger vers l'état auth qui correspond à la page de connexion.

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

Je pense que vous avez besoin d’un service qui gère le processus d’authentification (et son stockage).

Dans ce service, vous aurez besoin de méthodes de base:

  • isAuthenticated()
  • login()
  • logout()
  • etc ...

Ce service devrait être injecté dans vos contrôleurs de chaque module:

  • Dans votre section de tableau de bord, utilisez ce service pour vérifier si l'utilisateur est authentifié (méthode service.isAuthenticated()). sinon, rediriger vers/login
  • Dans votre section de connexion, utilisez simplement les données du formulaire pour authentifier l'utilisateur via votre méthode service.login().

Le projet angular-app et plus particulièrement module de sécurité , basé sur le génial HTTP Auth Interceptor Module , constitue un bon exemple de ce comportement.

J'espère que cela t'aides

22
Cétia

J'ai créé ce module pour aider à rendre ce processus un morceau de gâteau

Vous pouvez faire des choses comme:

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

Ou aussi

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

C'est tout neuf mais mérite le détour!

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

21
Rafael Vidaurre

Je voulais partager une autre solution fonctionnant avec le routeur ui 1.0.0.X

Comme vous le savez peut-être, stateChangeStart et stateChangeSuccess sont maintenant obsolètes. https://github.com/angular-ui/ui-router/issues/2655

Au lieu de cela, vous devriez utiliser $ transitions http://angular-ui.github.io/ui-router/1.0.0-alpha.1/interfaces/transition.ihookregistry.html

Voici comment je l'ai réalisé:

D'abord j'ai et AuthService avec quelques fonctions utiles

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

Ensuite, j'ai cette configuration:

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

Vous pouvez voir que j'utilise

data: {
   authRequired: true
}

pour marquer l'état accessible uniquement si est authentifié.

puis, sur le . run , j'utilise les transitions pour vérifier l'état de l'autheticated

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

Je construis cet exemple en utilisant du code trouvé dans la documentation de $ transitions. Je suis assez nouveau avec le routeur UI, mais cela fonctionne.

J'espère que ça peut aider n'importe qui.

14
Sergio Fernandez

Voici comment nous sommes sortis de la boucle de routage infinie et avons toujours utilisé $state.go au lieu de $location.path

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

J'ai une autre solution: cette solution fonctionne parfaitement lorsque vous avez uniquement le contenu à afficher lorsque vous êtes connecté. Définissez une règle permettant de vérifier si vous êtes connecté et son chemin d'accès aux itinéraires de liste blanche.

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

Dans mon exemple, je demande si je ne suis pas connecté et si l'itinéraire actuel que je souhaite acheminer ne fait pas partie de `/ login ', car mes itinéraires dans la liste blanche sont les suivants:

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

j'ai donc un accès instantané à ces deux itinéraires et tous les autres itinéraires seront vérifiés si vous êtes en ligne.

Voici tout mon fichier de routage pour le module de connexion

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 */ } est la syntaxe ES6, utilisez à la place function() { /* code */ }

2
Chris Incoqnito

Tout d'abord, vous aurez besoin d'un service que vous pouvez injecter dans vos contrôleurs et qui donne une idée de l'état d'authentification de l'application. La persistance des détails d'authentification avec le stockage local est une façon décente de s'en approcher.

Ensuite, vous devrez vérifier l'état de l'authentification juste avant que l'état ne change. Dans la mesure où votre application contient des pages à authentifier et d'autres non, créez un itinéraire parent vérifiant l'authentification et définissez toutes les autres pages le nécessitant comme un enfant de ce parent.

Enfin, vous aurez besoin d'un moyen de savoir si votre utilisateur actuellement connecté peut effectuer certaines opérations. Ceci peut être réalisé en ajoutant une fonction 'can' à votre service d'authentification. Can prend deux paramètres: - action - obligatoire - (c'est-à-dire "manage_dashboards" ou "create_new_dashboard") - objet - facultatif - objet en cours d'utilisation. Par exemple, si vous avez un objet de tableau de bord, vous souhaiterez peut-être vérifier si dashboard.ownerId === logsInUser.id. (Bien sûr, les informations transmises par le client ne doivent jamais être approuvées et vous devez toujours vérifier cela sur le serveur avant de les écrire dans votre base de données).

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

** AVERTISSEMENT: Le code ci-dessus est un pseudo-code et ne comporte aucune garantie **

2
colefner

Utilisez $ http Interceptor

En utilisant un intercepteur $ http, vous pouvez envoyer des en-têtes à Back-end ou inversement et effectuer vos vérifications de cette façon.

Excellent article sur $ http interceptors

Exemple:

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

Mettez ceci dans votre fonction .config ou .run.

2
TSlegaitis