web-dev-qa-db-fra.com

Authentification AngularJS + API RESTful

Communication côté client angulaire + REST avec API pour authentification/(re) routage

Cela a été couvert dans quelques questions différentes, et dans quelques tutoriels différents, mais toutes les ressources précédentes que j'ai rencontrées n'ont pas tout à fait cliché.

En bref, je dois

  • Connectez-vous via POST de http://client.foo à http://api.foo/login
  • Avoir une interface utilisateur/un état de connexion "connecté" pour l'utilisateur qui fournit une route logout
  • Pouvoir "mettre à jour" l'interface utilisateur lorsque l'utilisateur se déconnecte/se déconnecte. Cela a été le plus frustrant
  • Sécurisez mes itinéraires pour vérifier l'état authentifié (s'ils en ont besoin) et redirigez l'utilisateur vers la page de connexion en conséquence

Mes problèmes sont

  • Chaque fois que je navigue sur une page différente, je dois passer un appel à api.foo/status pour déterminer si un utilisateur est connecté ou non. (ATM j'utilise Express pour les itinéraires) Cela provoque un hoquet comme Angular détermine des choses comme ng-show="user.is_authenticated"
  • Lorsque je me connecte/déconnecte avec succès, je dois actualiser la page (je ne veux pas être obligé de le faire) afin de renseigner des éléments tels que {{user.first_name}} ou, dans le cas d'une déconnexion, vide 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 l'esprit

  • Il semble que chaque tutoriel repose sur une solution de base de données (beaucoup de Mongo, Couch, PHP + MySQL, à l'infini) et qu'aucun ne se fie uniquement à la communication avec une API RESTful pour conserver les états connectés. Une fois connectés, des POST/GET supplémentaires sont envoyés avec withCredentials:true, alors ce n'est pas la question
  • Je ne trouve AUCUN exemple/tutoriel/repos faisant Angular + REST + Auth, sans un langage backend.

Je ne suis pas trop fier

Certes, je suis nouveau sur Angular, et je ne serais pas surpris si je l’aborde de manière ridicule; Je serais ravi si quelqu'un suggérait une alternative, même si c'était de la soupe aux noix.

J'utilise Express surtout parce que j'aime beaucoup Jade et Stylus— je ne suis pas marié au routage Express 'et le laisserai tomber si ce que je veux faire n'est possible qu'avec le routage d'Angular.

Merci d'avance pour toute aide que n'importe qui peut fournir. Et s'il vous plaît ne me demandez pas à Google, parce que j'ai environ 26 pages de liens violets. ;-)


1Cette solution s'appuie sur la maquette $ httpBackend d'Angular, et on ne sait pas trop comment la faire parler à un vrai serveur.

2C'était le plus proche, mais comme j'ai une API existante sur laquelle je dois m'authentifier, je ne pouvais pas utiliser la 'localStrategy' du passeport, et cela me semblait fou écrire un service OAUTH ... que je ne voulais utiliser que.

71
couzzi

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

La sécurité dans une application Web frontale est simplement une mesure de départ pour arrêter Joe Public. Toutefois, tout utilisateur possédant certaines connaissances Web peut la contourner, vous devez donc toujours disposer du côté serveur de la sécurité.

La principale préoccupation concernant la sécurité dans angular est la sécurité de la route, heureusement lorsque vous définissez une route dans angular vous créez un objet, un objet pouvant avoir d'autres propriétés La pierre angulaire de mon approche consiste à ajouter un objet de sécurité à cet objet route, qui définit en gros les rôles que doit posséder l’utilisateur pour pouvoir 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 est centré sur un service d’autorisation qui vérifie si l’utilisateur dispose des autorisations requises. Ce service résume les préoccupations des autres parties de cette solution relatives à l'utilisateur et à son autorisation réelle qui aurait été extraite du serveur lors de la connexion. Bien que le code soit assez détaillé, il est expliqué en détail dans mon article. Cependant, il gère essentiellement le contrôle des autorisations et deux modes d’autorisation. La première est que l'utilisateur doit avoir au moins une des autorisations définies, la seconde est que l'utilisateur doit avoir toutes les autorisations 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 que la sécurité d'un itinéraire est sécurisée, vous avez besoin d'un moyen de déterminer si un utilisateur peut accéder à l'itinéraire lorsqu'un changement d'itinéraire a été démarré. Pour ce faire, nous interceptons la demande de changement d'itinéraire, 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 l'itinéraire par un 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 cela remplace la route actuelle (celle qu’ils n’ont pas le droit de voir) avec la route vers laquelle nous les redirigeons. Ceci arrête tout puis retourne à l’itinéraire non autorisé.

Maintenant, nous pouvons intercepter des routes, nous pouvons faire plusieurs choses intéressantes, y compris redirection après une connexion si un utilisateur atterrit sur une route pour laquelle il doit être connecté.

La deuxième partie de la solution consiste à pouvoir masquer/afficher un élément d'interface utilisateur à l'utilisateur en fonction de ses droits. Ceci est réalisé via 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 voudriez alors un élément comme celui-ci:

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

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

34
Jon

J'ai écrit un module AngularJS pour serApp qui fait à peu près tout ce que vous demandez. Vous pouvez soit:

  1. Modifiez le module et attachez les fonctions à votre propre API, ou
  2. Utilisez le module avec l'API de gestion des utilisateurs, serApp

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

Il prend en charge les itinéraires protégés/publics, le réacheminement lors de la connexion/déconnexion, les pulsations pour les vérifications d'état, stocke le jeton de session dans un cookie, des événements, etc.

Si vous voulez essayer UserApp, prenez le cours sur Codecademy .

Voici quelques exemples de son fonctionnement:

  • Formulaire de connexion avec traitement 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 traitement 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});
    

    La route .otherwise() doit être définie sur l'emplacement de redirection de vos utilisateurs 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 la route de connexion)

  • Propriétés de l'utilisateur d'accès:

    Les informations utilisateur sont accessibles via le service user, par exemple: user.current.email

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

  • Cacher les éléments qui ne devraient être visibles que lors de la connexion:

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

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

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

Et pour vous authentifier auprès de vos services d'arrière-plan, utilisez simplement user.token() pour obtenir le jeton de session et envoyez-le avec la demande AJAX. À l'arrière, utilisez - API UserApp (si vous utilisez UserApp) pour vérifier si le jeton est valide ou non.

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

5

Je n'ai pas utilisé $ resource parce que je viens de créer manuellement mes appels de service pour mon application. Cela dit, j’ai géré la connexion en disposant d’un service qui dépend de tous les autres services qui obtiennent une sorte de données d’initialisation. Lorsque la connexion réussit, elle déclenche l'initialisation de tous les services.

Dans le périmètre de mon contrôleur, je regarde les informations loginServiceInformation et renseigne 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’Anngular et j’ai simplement un masquage-ng basé sur le booléen connecté indiqué, il affiche du texte pour demander la connexion ou bien la div avec l’attribut ng-view (donc s’il n’est pas connecté immédiatement après la connexion, vous êtes sur la bonne page, je charge actuellement 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 parties 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 le contrôleur de connexion défini avec un contrôleur ng plus haut dans le DOM afin de pouvoir modifier la zone de corps de ma page en fonction de la variable enregistrée.

Remarque Je n'ai pas encore implémenté la validation de formulaire ici. De plus, certes, il est toujours assez frais pour Angular, donc tout pointeur vers ce message est le bienvenu. Bien que cela ne réponde pas directement à la question car il ne s'agit pas d'une implémentation basée sur RESTful, je pense que la même chose peut être faite. être adapté à $ resources car il est construit sur des appels $ http.

4
shaunhusain

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

repo Github ng-login

Plunker

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

(1) app.js: Création de constantes d'authentification pour la définition d'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 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. La mise en œuvre 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 parent: Considérez ceci comme la fonction "principale" de votre application, tous les contrôleurs héritent de ce contrôleur et constituent la colonne vertébrale de l'authentification. de cette application.

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

(5) Contrôle d'accès: Pour refuser l'accès sur certaines routes, vous devez mettre en oeuvre 2 étapes:

a) Ajoutez les données des rôles autorisés à accéder à chaque route, sur le service $ stateProvider du routeur ui, comme indiqué ci-dessous (il en va de même 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') ajoutez 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'authentification: Ceci est implémenté, mais ne peut pas être vérifié sur la portée de ce code. Après chaque demande $ http, cet intercepteur vérifie le code d'état. Si l'un des éléments ci-dessous est renvoyé, 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 lié à la saisie automatique des données de formulaire, comme indiqué dans le premier article, peut facilement être évité en ajoutant la directive incluse dans directives.js.

P.S.2 Ce code peut être facilement modifié par l'utilisateur afin de permettre la visualisation de différents itinéraires ou l'affichage de contenu non destiné à être affiché. La logique DOIT être implémentée côté serveur, c'est juste un moyen de montrer les choses correctement sur votre ng-app.

4
Alex Arvanitidis