Authentification AngularJS + API RESTful

Angulaire+Reposante côté Client de Communication w/ API pour Auth/(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 frappé le clou sur la tête.

dans une coque de noix, je dois

  • Se connecter par la poste de http://client.foo à http://api.foo/login
  • Avoir un État GUI/component" connecté "pour l'utilisateur qui fournit une" route "151930920 1519250920"
  • peut "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 authentifié(s'ils en ont besoin) et rediriger l'utilisateur vers la page de connexion en conséquence

Mes numéros sont

  • chaque fois que je navigue vers une page différente, je dois faire l'appel à api.foo/status pour déterminer si oui ou non l'utilisateur est connecté. (ATM j'utilise Express pour les routes) cela provoque un hoquet comme angulaire détermine des choses comme ng-show="user.is_authenticated"
  • quand je me connecte/déconnexion avec succès, je dois rafraîchir la page (je ne veux pas avoir à le faire) afin de peupler des choses comme {{user.first_name}} , ou dans le 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 chaque tutoriel s'appuie sur une base de données (beaucoup de Mongo, Couch, PHP + MySQL, ad infinitum) solution, et aucun ne s'appuie purement sur communication avec une API RESTful pour maintenir les États connectés. Une fois connecté, les messages/GETs supplémentaires sont envoyés avec withCredentials:true , donc ce n'est pas le problème
  • Je ne trouve pas d'exemples/tutoriels/repos qui font Angular+REST+Auth, sans a backend language.

Je ne suis pas trop fier

Certes, je suis nouveau à L'Angular, et ne serait pas surpris si j'aborde cela d'une manière ridicule; Je serais ravie si quelqu'un suggérait une alternative, même si c'est de la soupe aux noix.

j'utilise Express principalement parce que j'aime vraiment Jade et Stylus - Je ne suis pas marié au routage Express et je vais l'abandonner si ce que je veux faire est seulement possible avec le routage D'Angular.

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


1 Cette solution repose sur le mock $httpBackend d'Angular, et il n'est pas clair comment le faire parler à un vrai serveur.

2 C'était le plus proche, mais puisque j'ai une API existante avec laquelle je dois m'authentifier, Je ne pouvais pas utiliser la 'stratégie locale' de passport, et il me semblait insensé pour écrire un service OAUTH...que je suis seule à l'intention d'utiliser.

70
demandé sur Puspendu Banerjee 2013-08-20 04:53:31

4 réponses

Ceci est pris à partir de mon blog sur l'url de la route de l'autorisation et de l'élément de sécurité ici mais je vais brièvement résumés les principaux points :-)

la sécurité dans l'application web de frontend est simplement une mesure de départ pour arrêter Joe Public, cependant tout utilisateur avec une certaine connaissance du web peut le contourner de sorte que vous devriez toujours avoir la sécurité côté serveur ainsi.

la principale préoccupation concernant les problèmes de sécurité dans angular is route security, heureusement lorsque vous définissez une route en 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 dans lesquels l'utilisateur doit se trouver 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'approche globale est centrée sur un service d'autorisation qui vérifie essentiellement si l'utilisateur dispose des autorisations requises. Ce service résumé les préoccupations à l'écart des autres parties de cette solution pour le faire avec l'utilisateur et de leur autorisation qui aurait été récupérées à partir du serveur lors de la connexion. Alors que le code est tout à fait verbeux, il est pleinement expliqué dans mon billet de blog. Toutefois, il traite essentiellement de la vérification des autorisations et de deux modes d'autorisation. La première est que l'utilisateur doit avoir au moins des autorisations définies, la deuxième est l'utilisateur doit disposer de 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 l'itinéraire de sécurité vous avez besoin d'un moyen de déterminer si un utilisateur peut accéder à la route lors d'un changement d'itinéraire a été commencé. Pour ce faire, nous interceptons la demande de changement de route, en examinant l'objet route (avec notre nouvel objet access dessus) 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 () " comme cela remplace l'itinéraire actuel (celui qu'ils n'ont pas eu droits de voir) avec la route vers laquelle nous les redirigeons. Cet arrêt n'importe quel puis naviguer de nouveau à la route non autorisée.

maintenant nous pouvons intercepter les routes nous pouvons faire pas mal de choses fraîches, y compris redirection après une connexion si un utilisateur atterri sur une route qu'ils devaient être connectés pour.

la deuxième partie de la solution est d'être en mesure de cacher/montrer L'élément UI à l'utilisateur en fonction de ses droits. Ceci est réalisé via un 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 seriez alors sûr d'un élément comme cela:

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

Lisez mon article complet pour une présentation plus détaillée de l'approche.

34
répondu Jon 2015-08-04 16:32:48

j'ai écrit un module AngularJS pour UserApp 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. utiliser 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 réacheminement sur la connexion/déconnexion, les battements de cœur pour les vérifications de statut, stocke le token de session dans un cookie, événements, etc.

si vous voulez donner à UserApp un essai, prendre le cours sur Codecademy .

voici quelques exemples de comment cela fonctionne:

  • 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 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 les routes qui doivent être publiques, et quelle route qui 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 à l'endroit où vous voulez que vos utilisateurs soient redirigés après la connexion. Exemple:

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

  • Journal de lien:

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

    (termine la session et redirige vers la route de connexion)

  • l'Accès des propriétés de l'utilisateur:

    L'information Utilisateur

    est accessible en utilisant le service user , E. g: user.current.email

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

  • Masquer les éléments qui ne devraient être visibles qu'une fois connectés:

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

  • afficher un élément basé sur les permissions:

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

et de vous authentifier services d'arrière-plan, il suffit d'utiliser user.token() pour obtenir le jeton de session et l'envoyer avec la requête AJAX. À la fin, utilisez L'API USERAPP (si vous utilisez UserApp) pour vérifier si le token est valide ou non.

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

5
répondu Timothy E. Johansson 2013-12-17 00:47:33

Je n'ai pas utilisé $resource parce que je suis juste la main artisanat mes appels de service pour mon application. Cela dit, j'ai géré login en ayant un service qui dépend de tous les autres services qui obtiennent une sorte de données d'initialisation. Lorsque le login réussit, il déclenche l'initialisation de tous les services.

dans le cadre de mon contrôleur je regarde le loginServiceInformation et remplir certaines propriétés du modèle en conséquence (pour déclencher le ng-afficher/masquer). En ce qui concerne le routage, J'utilise Angular construit dans le routage et j'ai simplement un cache ng basé sur le booléen loggedIn montré ici, il montre le texte pour demander la connexion ou bien le div avec l'attribut de vue ng (donc si pas connecté immédiatement après la connexion vous êtes sur la page correcte, actuellement je charge des données pour toutes les vues, mais je crois 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"]);

the 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 que utilise les parties ci-dessus pour compléter le tableau:

<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 controller login défini avec un controller ng plus haut dans le DOM de sorte que je puisse changer la zone de corps de ma page basée sur la variable loggedIn.

Note je n'ai pas encore implémenté la validation de formulaire ici. Aussi, il est vrai que encore assez frais à L'angulaire de sorte que tous les indicateurs de choses dans ce post sont les bienvenus. Bien que cela ne réponde pas directement à la question puisqu'il n'est pas Implémentation basée RESTful je pense que la même chose peut être adaptée à $resources puisqu'elle est construite en plus de $http calls.

4
répondu shaunhusain 2013-08-20 02:40:49

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

ng-login dépôt Github

Plunker

je vais essayer de l'expliquer aussi bien que possible, j'espère aider certains d'entre vous:

(1) app.js: création des constantes d'authentification sur 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 de Demenagement: Toutes les fonctions suivantes sont mises en œuvre dans auth.js service. Le $service http est utilisé pour communiquer avec le serveur pour que les procédures d'authentification. Contient également des fonctions sur l'autorisation, c'est 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 garder 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 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 sur certaines routes 2 étapes doivent être mis en œuvre:

a) Ajouter les données des rôles autorisés pour accéder à chaque route, sur le service $stateProvider d'ui router comme peut être vu ci-dessous (même 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') ajoute la fonction pour empêcher la modification 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) Auth intercepteur: Ceci est mis en œuvre, mais ne peut pas être vérifiée sur le champ d'application de ce code. Après chaque requête $ http, cet intercepteur vérifie le code de statut, Si l'une des requêtes ci-dessous est retournée, 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 bug avec les données de formulaire autofill comme indiqué sur le 1er article peut être facilement évité en ajoutant la directive qui est incluse dans les directives.js.

P.S. 2 ce code peut être facilement modifié par l'utilisateur, pour permettre différents itinéraires pour être vu, ou afficher du contenu qui n'était pas destiné à être affiché. La logique doit être implémentée côté serveur, c'est juste une façon de montrer les choses correctement sur votre ng-app.

4
répondu Alex Arvanitidis 2015-03-05 10:35:03