71 votes

Authentification AngularJS + API RESTful

Communication côté client Angular+RESTful avec API pour l'authentification/le (re)routage

Ce sujet a été abordé dans différentes questions et dans différents tutoriels, mais toutes les ressources précédentes que j'ai rencontrées ne font pas le tour de la question.

En résumé, j'ai besoin de

  • Connexion via POST à partir de http://client.foo a http://api.foo/login
  • Disposer d'un état de l'interface graphique/du composant "connecté" pour l'utilisateur qui fournit une logout itinéraire
  • Pouvoir "mettre à jour" l'interface utilisateur lorsque l'utilisateur se déconnecte / se déconnecte. Cela a été le plus frustrant
  • Sécuriser mes routes pour vérifier l'état d'authentification (si nécessaire) et rediriger l'utilisateur vers la page de connexion en conséquence.

Mes problèmes sont les suivants

  • Chaque fois que je navigue vers une page différente, je dois faire l'appel à api.foo/status pour déterminer si l'utilisateur est connecté ou non. (ATM Je suis en train d'utiliser Express pour les routes) Cela provoque un hoquet car Angular détermine des choses comme ng-show="user.is_authenticated"
  • Lorsque je réussis à me connecter ou à me déconnecter, je dois rafraîchir la page (je ne veux pas avoir à le faire) afin de remplir des éléments tels que {{user.first_name}} ou, en cas de déconnexion, vider cette valeur.

    // Sample response from /status if successful

    { customer: {...}, is_authenticated: true, authentication_timeout: 1376959033, ... }

Ce que j'ai essayé

Pourquoi j'ai l'impression de perdre la tête

  • Il semble que tous les tutoriels s'appuient sur une base de données (Mongo, Couch, PHP+MySQL, etc.), mais aucun ne s'appuie uniquement sur la communication avec une API RESTful pour conserver les états de connexion. Une fois connecté, des POST/GET supplémentaires sont envoyés avec withCredentials:true donc ce n'est pas le problème
  • Je ne peux pas trouver d'exemples/tutoriels/positions qui font Angular+REST+Auth, sans langage backend.

Je ne suis pas trop fier

Certes, je suis novice en matière d'Angular, et je ne serais pas surpris si j'aborde cette question de manière ridicule ; je serais ravi que quelqu'un me suggère une alternative, même si elle est parfaite.

J'utilise Express surtout parce que j'aime vraiment Jade y Stylus - Je ne suis pas marié à la Express Je suis prêt à y renoncer si ce que je veux faire n'est possible qu'avec le routage d'Angular.

Merci d'avance pour toute aide que vous pourrez m'apporter. Et ne me demandez pas de chercher sur Google, car j'ai environ 26 pages de liens violets ;-)


1 Cette solution repose sur l'objet fantaisie $httpBackend d'Angular, et la façon de le faire communiquer avec un vrai serveur n'est pas claire.

2 C'est ce qui s'en rapprochait le plus, mais comme j'ai une API existante avec laquelle je dois m'authentifier, je ne pouvais pas utiliser la "stratégie locale" de Passeport, et il semblait que la stratégie locale n'était pas la bonne. fou pour écrire un service OAUTH... que j'étais le seul à vouloir utiliser.

34voto

Jon Samwell Points 1468

Ceci est tiré de mon article de blog sur l'autorisation de la route url et la sécurité des éléments. aquí mais je vais résumer brièvement les points principaux :-)

La sécurité de l'application web frontale n'est qu'une mesure de départ pour arrêter Joe Public. Cependant, n'importe quel utilisateur ayant quelques connaissances du web peut la contourner ; il faut donc toujours prévoir une sécurité côté serveur également.

La principale préoccupation en matière de sécurité dans Angular est la sécurité des routes. Heureusement, lorsque vous définissez une route dans Angular, vous créez un objet, un objet qui peut avoir d'autres propriétés. La pierre angulaire de mon approche est d'ajouter un objet de sécurité à cet objet route qui définit essentiellement les rôles que l'utilisateur doit avoir pour être en mesure d'accéder à une route particulière.

 // route which requires the user to be logged in and have the 'Admin' or 'UserManager' permission
    $routeProvider.when('/admin/users', {
        controller: 'userListCtrl',
        templateUrl: 'js/modules/admin/html/users.tmpl.html',
        access: {
            requiresLogin: true,
            requiredPermissions: ['Admin', 'UserManager'],
            permissionType: 'AtLeastOne'
        });

L'ensemble de l'approche s'articule autour d'un service d'autorisation qui vérifie essentiellement si l'utilisateur dispose des autorisations requises. Ce service permet d'abstraire les préoccupations des autres parties de cette solution concernant l'utilisateur et ses autorisations réelles qui auraient été récupérées du serveur lors de la connexion. Bien que le code soit assez verbeux, il est expliqué en détail dans mon article de blog. Cependant, il gère essentiellement la vérification des autorisations et deux modes d'autorisation. Le premier est que l'utilisateur doit avoir au moins une des permissions définies, le second est que l'utilisateur doit avoir toutes les permissions définies.

angular.module(jcs.modules.auth.name).factory(jcs.modules.auth.services.authorization, [  
'authentication',  
function (authentication) {  
 var authorize = function (loginRequired, requiredPermissions, permissionCheckType) {
    var result = jcs.modules.auth.enums.authorised.authorised,
        user = authentication.getCurrentLoginUser(),
        loweredPermissions = [],
        hasPermission = true,
        permission, i;

    permissionCheckType = permissionCheckType || jcs.modules.auth.enums.permissionCheckType.atLeastOne;
    if (loginRequired === true && user === undefined) {
        result = jcs.modules.auth.enums.authorised.loginRequired;
    } else if ((loginRequired === true && user !== undefined) &&
        (requiredPermissions === undefined || requiredPermissions.length === 0)) {
        // Login is required but no specific permissions are specified.
        result = jcs.modules.auth.enums.authorised.authorised;
    } else if (requiredPermissions) {
        loweredPermissions = [];
        angular.forEach(user.permissions, function (permission) {
            loweredPermissions.push(permission.toLowerCase());
        });

        for (i = 0; i < requiredPermissions.length; i += 1) {
            permission = requiredPermissions[i].toLowerCase();

            if (permissionCheckType === jcs.modules.auth.enums.permissionCheckType.combinationRequired) {
                hasPermission = hasPermission && loweredPermissions.indexOf(permission) > -1;
                // if all the permissions are required and hasPermission is false there is no point carrying on
                if (hasPermission === false) {
                    break;
                }
            } else if (permissionCheckType === jcs.modules.auth.enums.permissionCheckType.atLeastOne) {
                hasPermission = loweredPermissions.indexOf(permission) > -1;
                // if we only need one of the permissions and we have it there is no point carrying on
                if (hasPermission) {
                    break;
                }
            }
        }

        result = hasPermission ?
                 jcs.modules.auth.enums.authorised.authorised :
                 jcs.modules.auth.enums.authorised.notAuthorised;
    }

    return result;
};

Maintenant qu'une route est sécurisée, vous avez besoin d'un moyen de déterminer si un utilisateur peut accéder à la route lorsqu'un changement de route a été lancé. Pour ce faire, nous interceptons la demande de changement de route, examinons l'objet route (avec notre nouvel objet d'accès) et si l'utilisateur ne peut pas accéder à la vue, nous remplaçons la route par une autre.

angular.module(jcs.modules.auth.name).run([  
    '$rootScope',
    '$location',
    jcs.modules.auth.services.authorization,
    function ($rootScope, $location, authorization) {
        $rootScope.$on('$routeChangeStart', function (event, next) {
            var authorised;
            if (next.access !== undefined) {
                authorised = authorization.authorize(next.access.loginRequired,
                                                     next.access.permissions,
                                                     next.access.permissionCheckType);
                if (authorised === jcs.modules.auth.enums.authorised.loginRequired) {
                    $location.path(jcs.modules.auth.routes.login);
                } else if (authorised === jcs.modules.auth.enums.authorised.notAuthorised) {
                    $location.path(jcs.modules.auth.routes.notAuthorised).replace();
                }
            }
        });
    }]);

La clé ici est vraiment le '.replace()' car il remplace la route actuelle (celle qu'ils n'ont pas le droit de voir) par la route vers laquelle nous les redirigeons. Cela les empêche de retourner sur la route non autorisée.

Maintenant que nous pouvons intercepter des routes, nous pouvons faire plusieurs choses intéressantes, notamment redirection après une connexion si un utilisateur a atterri sur une route pour laquelle il devait être connecté.

La deuxième partie de la solution consiste à pouvoir masquer/afficher des éléments de l'interface utilisateur à l'utilisateur en fonction de ses droits. Ceci est réalisé par une simple directive.

angular.module(jcs.modules.auth.name).directive('access', [  
        jcs.modules.auth.services.authorization,
        function (authorization) {
            return {
              restrict: 'A',
              link: function (scope, element, attrs) {
                  var makeVisible = function () {
                          element.removeClass('hidden');
                      },
                      makeHidden = function () {
                          element.addClass('hidden');
                      },
                      determineVisibility = function (resetFirst) {
                          var result;
                          if (resetFirst) {
                              makeVisible();
                          }

                          result = authorization.authorize(true, roles, attrs.accessPermissionType);
                          if (result === jcs.modules.auth.enums.authorised.authorised) {
                              makeVisible();
                          } else {
                              makeHidden();
                          }
                      },
                      roles = attrs.access.split(',');

                  if (roles.length > 0) {
                      determineVisibility(true);
                  }
              }
            };
        }]);

Vous serez alors sûr d'un élément comme celui-ci :

 <button type="button" access="CanEditUser, Admin" access-permission-type="AtLeastOne">Save User</button>

Lire mon article de blog complet pour un aperçu beaucoup plus détaillé de l'approche.

5voto

J'ai écrit un Module AngularJS para UserApp qui fait à peu près tout ce que vous demandez. Vous pourriez soit :

  1. Modifier le module et attacher les fonctions à votre propre API, ou
  2. Utilisez le module avec l'API de gestion des utilisateurs, UserApp

https://github.com/userapp-io/userapp-angular

Il prend en charge les routes protégées/publiques, le reroutage en cas de connexion/déconnexion, les battements de cœur pour les contrôles d'état, le stockage du jeton de session dans un cookie, les événements, etc.

Si vous voulez faire l'essai de UserApp, suivez la procédure suivante cours sur Codecademy .

Voici quelques exemples de son fonctionnement :

  • Formulaire de connexion avec gestion des erreurs :

    <form ua-login ua-error="error-msg">
        <input name="login" placeholder="Username"><br>
        <input name="password" placeholder="Password" type="password"><br>
        <button type="submit">Log in</button>
        <p id="error-msg"></p>
    </form>
  • Formulaire d'inscription avec gestion des erreurs :

    <form ua-signup ua-error="error-msg">
      <input name="first_name" placeholder="Your name"><br>
      <input name="login" ua-is-email placeholder="Email"><br>
      <input name="password" placeholder="Password" type="password"><br>
      <button type="submit">Create account</button>
      <p id="error-msg"></p>
    </form>
  • Comment spécifier quelles routes doivent être publiques, et quelle route est le formulaire de connexion :

    $routeProvider.when('/login', {templateUrl: 'partials/login.html', public: true, login: true});
    $routeProvider.when('/signup', {templateUrl: 'partials/signup.html', public: true});

    El .otherwise() La route doit être définie comme l'endroit où vous souhaitez que vos utilisateurs soient redirigés après la connexion. Exemple :

    $routeProvider.otherwise({redirectTo: '/home'});

  • Lien de déconnexion :

    <a href="#" ua-logout>Log Out</a>

    (Termine la session et redirige vers l'itinéraire de connexion)

  • Accéder aux propriétés de l'utilisateur :

    L'accès aux informations sur l'utilisateur se fait à l'aide de la fonction user service, par exemple user.current.email

    Ou dans le modèle : <span>{{ user.email }}</span>

  • Masquer les éléments qui ne doivent être visibles que lorsque l'on est connecté :

    <div ng-show="user.authorized">Welcome {{ user.first_name }}!</div>

  • Afficher un élément en fonction des autorisations :

    <div ua-has-permission="admin">You are an admin</div>

Et pour s'authentifier auprès de vos services back-end, il suffit d'utiliser user.token() pour obtenir le jeton de session et l'envoyer avec la requête AJAX. Au niveau du back-end, utilisez la méthode API de l'application utilisateur (si vous utilisez UserApp) pour vérifier si le jeton est valide ou non.

Si vous avez besoin d'aide, faites-le moi savoir :)

4voto

shaunhusain Points 10933

Je n'ai pas utilisé $resource parce que j'élabore à la main mes appels de service pour mon application. Cela dit, j'ai géré la connexion en ayant un service qui dépend de tous les autres services qui reçoivent une sorte de données d'initialisation. Lorsque la connexion réussit, cela déclenche l'initialisation de tous les services.

Dans la portée de mon contrôleur, je regarde le loginServiceInformation et je remplis certaines propriétés du modèle en conséquence (pour déclencher le ng-show/hide approprié). En ce qui concerne le routage, j'utilise le routage intégré d'Angular et j'ai simplement un ng-hide basé sur le booléen loggedIn montré ici, il montre le texte pour demander la connexion ou bien la div avec l'attribut ng-view (donc si vous n'êtes pas connecté immédiatement après la connexion, vous êtes sur la bonne page, actuellement je charge les données pour toutes les vues mais je pense que cela pourrait être plus sélectif si nécessaire).

//Services
angular.module("loginModule.services", ["gardenModule.services",
                                        "surveyModule.services",
                                        "userModule.services",
                                        "cropModule.services"
                                        ]).service(
                                            'loginService',
                                            [   "$http",
                                                "$q",
                                                "gardenService",
                                                "surveyService",
                                                "userService",
                                                "cropService",
                                                function (  $http,
                                                            $q,
                                                            gardenService,
                                                            surveyService,
                                                            userService,
                                                            cropService) {

    var service = {
        loginInformation: {loggedIn:false, username: undefined, loginAttemptFailed:false, loggedInUser: {}, loadingData:false},

        getLoggedInUser:function(username, password)
        {
            service.loginInformation.loadingData = true;
            var deferred = $q.defer();

            $http.get("php/login/getLoggedInUser.php").success(function(data){
                service.loginInformation.loggedIn = true;
                service.loginInformation.loginAttemptFailed = false;
                service.loginInformation.loggedInUser = data;

                gardenService.initialize();
                surveyService.initialize();
                userService.initialize();
                cropService.initialize();

                service.loginInformation.loadingData = false;

                deferred.resolve(data);
            }).error(function(error) {
                service.loginInformation.loggedIn = false;
                deferred.reject(error);
            });

            return deferred.promise;
        },
        login:function(username, password)
        {
            var deferred = $q.defer();

            $http.post("php/login/login.php", {username:username, password:password}).success(function(data){
                service.loginInformation.loggedInUser = data;
                service.loginInformation.loggedIn = true;
                service.loginInformation.loginAttemptFailed = false;

                gardenService.initialize();
                surveyService.initialize();
                userService.initialize();
                cropService.initialize();

                deferred.resolve(data);
            }).error(function(error) {
                service.loginInformation.loggedInUser = {};
                service.loginInformation.loggedIn = false;
                service.loginInformation.loginAttemptFailed = true;
                deferred.reject(error);
            });

            return deferred.promise;
        },
        logout:function()
        {
            var deferred = $q.defer();

            $http.post("php/login/logout.php").then(function(data){
                service.loginInformation.loggedInUser = {};
                service.loginInformation.loggedIn = false;
                deferred.resolve(data);
            }, function(error) {
                service.loginInformation.loggedInUser = {};
                service.loginInformation.loggedIn = false;
                deferred.reject(error);
            });

            return deferred.promise;
        }
    };
    service.getLoggedInUser();
    return service;
}]);

//Controllers
angular.module("loginModule.controllers", ['loginModule.services']).controller("LoginCtrl", ["$scope", "$location", "loginService", function($scope, $location, loginService){

    $scope.loginModel = {
                        loadingData:true,
                        inputUsername: undefined,
                        inputPassword: undefined,
                        curLoginUrl:"partials/login/default.html",
                        loginFailed:false,
                        loginServiceInformation:{}
                        };

    $scope.login = function(username, password) {
        loginService.login(username,password).then(function(data){
            $scope.loginModel.curLoginUrl = "partials/login/logoutButton.html";
        });
    }
    $scope.logout = function(username, password) {
        loginService.logout().then(function(data){
            $scope.loginModel.curLoginUrl = "partials/login/default.html";
            $scope.loginModel.inputPassword = undefined;
            $scope.loginModel.inputUsername = undefined;
            $location.path("home");
        });
    }
    $scope.switchUser = function(username, password) {
        loginService.logout().then(function(data){
            $scope.loginModel.curLoginUrl = "partials/login/loginForm.html";
            $scope.loginModel.inputPassword = undefined;
            $scope.loginModel.inputUsername = undefined;
        });
    }
    $scope.showLoginForm = function() {
        $scope.loginModel.curLoginUrl = "partials/login/loginForm.html";
    }
    $scope.hideLoginForm = function() {
        $scope.loginModel.curLoginUrl = "partials/login/default.html";
    }

    $scope.$watch(function(){return loginService.loginInformation}, function(newVal) {
        $scope.loginModel.loginServiceInformation = newVal;
        if(newVal.loggedIn)
        {
            $scope.loginModel.curLoginUrl = "partials/login/logoutButton.html";
        }
    }, true);
}]);

angular.module("loginModule", ["loginModule.services", "loginModule.controllers"]);

Le HTML

<div style="height:40px;z-index:200;position:relative">
    <div class="well">
        <form
            ng-submit="login(loginModel.inputUsername, loginModel.inputPassword)">
            <input
                type="text"
                ng-model="loginModel.inputUsername"
                placeholder="Username"/><br/>
            <input
                type="password"
                ng-model="loginModel.inputPassword"
                placeholder="Password"/><br/>
            <button
                class="btn btn-primary">Submit</button>
            <button
                class="btn"
                ng-click="hideLoginForm()">Cancel</button>
        </form>
        <div
            ng-show="loginModel.loginServiceInformation.loginAttemptFailed">
            Login attempt failed
        </div>
    </div>
</div>

Le HTML de base qui utilise les éléments ci-dessus pour compléter l'image :

<body ng-controller="NavigationCtrl" ng-init="initialize()">
        <div id="outerContainer" ng-controller="LoginCtrl">
            <div style="height:20px"></div>
            <ng-include src="'partials/header.html'"></ng-include>
            <div  id="contentRegion">
                <div ng-hide="loginModel.loginServiceInformation.loggedIn">Please login to continue.
                <br/><br/>
                This new version of this site is currently under construction.
                <br/><br/>
                If you need the legacy site and database <a href="legacy/">click here.</a></div>
                <div ng-view ng-show="loginModel.loginServiceInformation.loggedIn"></div>
            </div>
            <div class="clear"></div>
            <ng-include src="'partials/footer.html'"></ng-include>
        </div>
    </body>

J'ai défini le contrôleur de connexion avec un ng-controller plus haut dans le DOM afin de pouvoir modifier le corps de ma page en fonction de la variable loggedIn.

Notez que je n'ai pas encore implémenté la validation de formulaire ici. J'admets également que je suis encore assez novice en matière d'Angular, donc toute indication sur les choses dans ce post est la bienvenue. Bien que cela ne réponde pas directement à la question puisqu'il ne s'agit pas d'une implémentation basée sur RESTful, je pense que la même chose peut être adaptée à $resources puisqu'elle est construite sur des appels $http.

4voto

J'ai créé un repo github qui résume cet article en gros : https://medium.com/opinionated-angularjs/techniques-for-authentication-in-angularjs-applications-7bbf0346acec

ng-login Dépôt Github

Plunker

Je vais essayer d'expliquer le mieux possible, en espérant aider certains d'entre vous :

(1) app.js : Création de constantes d'authentification lors de la définition de l'application

var loginApp = angular.module('loginApp', ['ui.router', 'ui.bootstrap'])
/*Constants regarding user login defined here*/
.constant('USER_ROLES', {
    all : '*',
    admin : 'admin',
    editor : 'editor',
    guest : 'guest'
}).constant('AUTH_EVENTS', {
    loginSuccess : 'auth-login-success',
    loginFailed : 'auth-login-failed',
    logoutSuccess : 'auth-logout-success',
    sessionTimeout : 'auth-session-timeout',
    notAuthenticated : 'auth-not-authenticated',
    notAuthorized : 'auth-not-authorized'
})

(2) Service d'authentification : Toutes les fonctions suivantes sont implémentées dans le service auth.js. Le service $http est utilisé pour communiquer avec le serveur pour les procédures d'authentification. Contient également des fonctions sur l'autorisation, c'est-à-dire si l'utilisateur est autorisé à effectuer une certaine action.

angular.module('loginApp')
.factory('Auth', [ '$http', '$rootScope', '$window', 'Session', 'AUTH_EVENTS', 
function($http, $rootScope, $window, Session, AUTH_EVENTS) {

authService.login() = [...]
authService.isAuthenticated() = [...]
authService.isAuthorized() = [...]
authService.logout() = [...]

return authService;
} ]);

(3) Session : Un singleton pour conserver les données de l'utilisateur. L'implémentation ici dépend de vous.

angular.module('loginApp').service('Session', function($rootScope, USER_ROLES) {

    this.create = function(user) {
        this.user = user;
        this.userRole = user.userRole;
    };
    this.destroy = function() {
        this.user = null;
        this.userRole = null;
    };
    return this;
});

(4) Contrôleur parental : Considérez ceci comme la fonction "principale" de votre application, tous les contrôleurs héritent de ce contrôleur, et c'est l'épine dorsale de l'authentification de cette application.

<body ng-controller="ParentController">
[...]
</body>

(5) Contrôle d'accès : Pour refuser l'accès à certaines routes, deux étapes doivent être mises en œuvre :

a) Ajouter les données des rôles autorisés à accéder à chaque route, sur le service $stateProvider du routeur ui comme on peut le voir ci-dessous (la même chose peut fonctionner pour ngRoute).

.config(function ($stateProvider, USER_ROLES) {
  $stateProvider.state('dashboard', {
    url: '/dashboard',
    templateUrl: 'dashboard/index.html',
    data: {
      authorizedRoles: [USER_ROLES.admin, USER_ROLES.editor]
    }
  });
})

b) Sur $rootScope.$on('$stateChangeStart') ajouter la fonction pour empêcher le changement d'état si l'utilisateur n'est pas autorisé.

$rootScope.$on('$stateChangeStart', function (event, next) {
    var authorizedRoles = next.data.authorizedRoles;
    if (!Auth.isAuthorized(authorizedRoles)) {
      event.preventDefault();
      if (Auth.isAuthenticated()) {
        // user is not allowed
        $rootScope.$broadcast(AUTH_EVENTS.notAuthorized);
      } else {d
        // user is not logged in
        $rootScope.$broadcast(AUTH_EVENTS.notAuthenticated);
      }
    }
});

(6) Intercepteur d'autorisation : Ceci est implémenté, mais ne peut pas être vérifié dans le cadre de ce code. Après chaque requête $http, cet intercepteur vérifie le code d'état, si l'un des codes ci-dessous est retourné, alors il diffuse un événement pour forcer l'utilisateur à se connecter à nouveau.

angular.module('loginApp')
.factory('AuthInterceptor', [ '$rootScope', '$q', 'Session', 'AUTH_EVENTS',
function($rootScope, $q, Session, AUTH_EVENTS) {
    return {
        responseError : function(response) {
            $rootScope.$broadcast({
                401 : AUTH_EVENTS.notAuthenticated,
                403 : AUTH_EVENTS.notAuthorized,
                419 : AUTH_EVENTS.sessionTimeout,
                440 : AUTH_EVENTS.sessionTimeout
            }[response.status], response);
            return $q.reject(response);
        }
    };
} ]);

P.S. Un bogue avec le remplissage automatique des données du formulaire, comme indiqué dans le premier article, peut être facilement évité en ajoutant la directive qui est incluse dans directives.js.

P.S.2 Ce code peut être facilement modifié par l'utilisateur, pour permettre à différentes routes d'être vues, ou afficher du contenu qui n'était pas censé être affiché. La logique DOIT être implémentée côté serveur, c'est juste un moyen d'afficher les choses correctement sur votre ng-app.

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