378 votes

AngularJS ui-router login authentification

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

Je suis en train de construire 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 seconde 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 angulaire et ui-router configurer pour gérer /login et /signup vues, et il y a un autre fichier dashboard.html pour la section du tableau de bord avec son application et ui-router pour gérer de nombreuses vues secondaires.

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 angulaires. Comment puis-je dire à l'application home de rediriger vers l'application dashboard ?

1 votes

Pouvez-vous partager un peu de code avec nous ?

6 votes

@Chancho Je pense que ce n'est pas une question de code, vraiment je ne sais pas quel code je devrais partager.

0 votes

Oui, merci de partager le code, question très générique...

610voto

HackedByChinese Points 18294

Je suis en train de faire une démo plus jolie et de nettoyer certains de ces services pour en faire un module utilisable, mais voici ce que j'ai trouvé. Il s'agit d'un processus complexe pour contourner certains obstacles, alors accrochez-vous. Vous devrez décomposer le tout en plusieurs parties.

Jetez un coup d'œil à ce plunk .

Tout d'abord, vous avez besoin d'un service pour stocker l'identité de l'utilisateur. J'appelle ce service principal . Il peut être vérifié pour voir si l'utilisateur est connecté et, sur demande, il peut résoudre un objet qui représente les informations essentielles sur l'identité de l'utilisateur. Cet objet peut être ce dont vous avez besoin, mais l'essentiel serait un nom d'affichage, un nom d'utilisateur, éventuellement un courriel, et les rôles auxquels l'utilisateur appartient (si cela s'applique à votre application). Principal dispose également de méthodes pour vérifier les rôles.

.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 dans lequel l'utilisateur veut aller, s'assure qu'il est connecté (si nécessaire ; pas nécessaire pour la connexion, la réinitialisation du mot de passe, etc. S'ils ne sont pas authentifiés, envoyez-les à la page de connexion. S'il est authentifié, mais que la vérification de son rôle échoue, il est renvoyé à une page de refus d'accès. 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 l'émission ui-router 's $stateChangeStart . Cela vous donne l'occasion d'examiner l'état actuel, l'état auquel ils veulent aller, et d'insérer votre contrôle d'autorisation. En cas d'échec, vous pouvez annuler la transition d'itinéraire ou passer à un autre 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 à la 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é la page ou si vous êtes tombé sur l'URL à partir d'un lien). En raison de la façon dont ui-router fonctionne, vous devez résoudre votre problème d'identité une fois, avant vos vérifications d'identité. Vous pouvez le faire en utilisant la fonction resolve dans la configuration de votre état. J'ai un état parent pour le site dont tous les états héritent, ce qui force le principal à être résolu avant toute autre chose.

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

Il y a un autre problème... resolve n'est appelé qu'une seule fois. Une fois que votre promesse de recherche d'identité est terminée, le délégué resolve ne sera plus exécuté. Nous devons donc effectuer les vérifications d'authentification à deux endroits : une fois en vertu de votre promesse d'identité qui se résout en resolve qui couvre la première fois que votre application se charge, et une fois en $stateChangeStart si la résolution a été effectuée, ce qui couvre tout le temps où vous naviguez dans les États.

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

  1. Nous vérifions, lors du chargement de l'application, si l'utilisateur est connecté.
  2. Nous enregistrons des informations sur l'utilisateur connecté.
  3. Nous les redirigeons vers l'état de connexion pour les états qui exigent que l'utilisateur soit connecté.
  4. Nous les redirigeons vers un état de refus d'accès s'ils n'ont pas l'autorisation d'y accéder.
  5. Nous disposons d'un mécanisme permettant de rediriger les utilisateurs vers l'état initial qu'ils ont demandé, si nous avons besoin qu'ils se connectent.
  6. Nous pouvons signer la sortie d'un utilisateur (il faut le faire de concert avec tout code client ou serveur qui gère votre ticket d'authentification).
  7. Nous ne de renvoyer les utilisateurs à la page de connexion chaque fois qu'ils rechargent leur navigateur ou qu'ils cliquent sur un lien.

Quelle est la suite des événements ? Vous pouvez organiser vos États en régions qui requièrent une connexion. Vous pouvez exiger des utilisateurs authentifiés/autorisés en ajoutant data con roles à ces états (ou à l'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 désormais contrôler, état par état, les utilisateurs qui peuvent accéder à un itinéraire. D'autres questions se posent ? Peut-être faire varier une partie seulement d'une vue selon que l'utilisateur est connecté ou non ? Aucun problème. Utilisez la fonction principal.isAuthenticated() ou même principal.isInRole() avec l'une des nombreuses façons d'afficher conditionnellement un modèle ou un élément.

Tout d'abord, injecter principal dans un contrôleur ou autre, et le coller au scope pour pouvoir l'utiliser facilement dans votre vue :

.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. Quoi qu'il en soit, dans votre exemple d'application, vous auriez un état pour la page d'accueil qui permettrait aux utilisateurs non authentifiés de s'y rendre. Ils pourraient avoir des liens vers les états d'inscription ou d'enregistrement, ou avoir ces formulaires intégrés dans cette page. Tout ce qui vous convient.

Les pages du tableau de bord pourraient toutes hériter d'un état qui exige que l'utilisateur soit connecté et, par exemple, qu'il soit un User membre du rôle. Toutes les autorisations dont nous avons parlé en découlent.

28 votes

Merci, cela m'a vraiment aidé à mettre au point mon propre code. Par ailleurs, si vous obtenez une boucle de routage infinie (bug de l'UI Router), essayez $location.path au lieu de $state.go .

2 votes

Cette réponse est excellente et m'a beaucoup aidé. Lorsque je définis user = principal dans mon contrôleur et que j'essaie d'appeler user.identity().name dans ma vue pour obtenir le nom de l'utilisateur actuellement connecté, je n'obtiens que l'objet promesse {then : fn, catch : fn, finally:} et non l'objet _identité réel. Si j'utilise user.identity.then(fn(user)) je peux obtenir l'objet utilisateur mais cela semble être beaucoup de code pour la vue, est-ce que j'ai raté quelque chose ?

4 votes

@Ir1sh Je résoudrais d'abord l'identité dans le contrôleur et l'assignerais à $scope.user dans votre then fonction. Vous pouvez toujours faire référence à user dans vos vues ; lorsqu'elle sera résolue, la vue sera mise à jour.

120voto

M.K. Safi Points 4202

Les solutions proposées jusqu'à présent sont inutilement compliquées, à mon avis. Il existe une solution plus simple. La solution la documentation de ui-router dit écouter $locationChangeSuccess et utiliser $urlRouter.sync() pour vérifier une transition d'état, l'arrêter ou la reprendre. Mais même cela ne fonctionne pas.

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

Solution 1 : écouter sur $locationChangeSuccess

Vous pouvez écouter $locationChangeSuccess et vous pouvez y effectuer une certaine logique, même une logique asynchrone. En fonction de cette logique, vous pouvez laisser la fonction renvoyer un état non défini, ce qui entraînera la poursuite normale de la transition d'état, ou vous pouvez faire $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')
      })
  })
})

Gardez à l'esprit que cela n'empêche pas le chargement de l'état cible, mais que cela redirige vers la page de connexion si l'utilisateur n'est pas autorisé. Ce n'est pas grave, puisque la protection réelle se trouve sur le serveur, de toute façon.

Solution 2 : utiliser l'état resolve

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

Vous rejetez en fait la promesse faite dans resolve si l'utilisateur n'est pas authentifié et le redirige vers la page de connexion.

Voici comment cela 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, celle-ci empêche le chargement de l'état cible.

42voto

sebest Points 31

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'application authentification qui est 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');
        }
      });
    }]
  );

22voto

Cétia Points 4347

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

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

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

Ce service doit être injecté dans les contrôleurs de chaque module :

  • Dans votre tableau de bord, utilisez ce service pour vérifier si l'utilisateur est authentifié ( service.isAuthenticated() Si ce n'est pas le cas, rediriger vers /login
  • Dans votre section de connexion, utilisez simplement les données du formulaire pour authentifier l'utilisateur par l'intermédiaire de la fonction service.login() méthode

Un bon exemple de ce comportement est le projet angular-app et en particulier son module de sécurité qui est basé sur l'awesome Module d'interception HTTP Auth

J'espère que cela vous aidera

21voto

Rafael Vidaurre Points 91

J'ai créé ce module pour faciliter ce processus.

Vous pouvez faire des choses comme :

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

Ou encore

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

C'est tout nouveau, mais ça vaut le coup d'œil !

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

2 votes

Qu'est-ce qui m'empêche de modifier la source au moment de l'exécution et de supprimer votre 'admin' || 'god' et de continuer ?

12 votes

J'espère que toutes les demandes de données nécessitant une autorisation sont également vérifiées au niveau du serveur.

24 votes

Il ne s'agit pas d'une sécurité, l'autorisation côté client ne l'est jamais car vous pouvez toujours modifier les valeurs. Vous pourriez même intercepter les réponses du serveur et les évaluer comme étant "autorisées". Le but des permissions/autorisations côté client est d'éviter de laisser l'utilisateur faire des choses interdites pour des raisons d'ergonomie. Par exemple, si vous gérez une action réservée aux administrateurs, même si l'utilisateur trompe malicieusement le client pour lui permettre d'envoyer une requête restreinte au serveur, ce dernier renverra toujours une réponse 401. Ceci est bien sûr toujours de la responsabilité de l'api implémentée @BenRipley en effet

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X