Authentification d'identité
La méthode d'authentification d'identité la plus courante consiste à se connecter à l'aide d'un nom d'utilisateur (ou d'un e-mail) et d'un mot de passe. Cela signifie mettre en place un formulaire de connexion afin que les utilisateurs puissent se connecter avec leurs informations personnelles. Le formulaire ressemble à ceci :
<form name="loginForm" ng-controller="LoginController" ng-submit="login(credentials)" novalidate> <label for="username">Username:</label> <input type="text" id="username" ng-model="credentials.username"> <label for="password">Password:</label> <input type="password" id="password" ng-model="credentials.password"> <button type="submit">Login</button> </form>
Comme il s'agit d'un formulaire alimenté par Angular, nous utilisons la directive ngSubmit pour déclencher la fonction lors du téléchargement du formulaire. Notez que nous transmettons les informations personnelles dans la fonction de formulaire de téléchargement au lieu d'utiliser directement l'objet $scope.credentials. Cela rend la fonction plus facile à tester unitairement et réduit le couplage de la fonction à la portée actuelle du contrôleur. Le contrôleur ressemble à ceci :
.controller('LoginController', function ($scope, $rootScope, AUTH_EVENTS, AuthService) { $scope.credentials = { username: '', password: '' }; $scope.login = function (credentials) { AuthService.login(credentials).then(function (user) { $rootScope.$broadcast(AUTH_EVENTS.loginSuccess); $scope.setCurrentUser(user); }, function () { $rootScope.$broadcast(AUTH_EVENTS.loginFailed); }); };javascript:void(0); })
Nous remarquons qu’il y a ici un manque de logique réelle. Ce contrôleur est conçu ainsi pour découpler la logique d'authentification du formulaire. C'est une bonne idée d'extraire autant de logique que possible de notre contrôleur et de tout mettre dans les services. Le contrôleur d'AngularJS ne doit gérer que les objets dans $scope (en utilisant la surveillance ou l'opération manuelle) au lieu d'assumer trop de tâches trop lourdes.
Notification des changements de session
L'authentification de l'identité affectera l'état de l'ensemble de l'application. Pour cette raison, je préfère utiliser des événements (en utilisant $broadcast) pour notifier les modifications de session utilisateur. C'est une bonne idée de définir tous les codes d'événement possibles à mi-chemin. J'aime utiliser des constantes pour ce faire :
.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' })
Une bonne caractéristique des constantes est qu'elles peuvent être injectées à d'autres endroits à volonté, tout comme les services. Cela rend les constantes facilement appelables par notre test unitaire. Les constantes vous permettent également de les renommer facilement ultérieurement sans avoir à modifier de nombreux fichiers. La même astuce fonctionne avec les rôles d'utilisateur :
.constant('USER_ROLES', { all: '*', admin: 'admin', editor: 'editor', guest: 'guest' })
Si vous souhaitez accorder les mêmes autorisations aux éditeurs et aux administrateurs, il vous suffit de remplacer « éditeur » par « admin ».
L'AuthService
La logique liée à l'authentification et à l'autorisation d'identité (contrôle d'accès) est mieux placée dans le même service :
.factory('AuthService', function ($http, Session) { var authService = {}; authService.login = function (credentials) { return $http .post('/login', credentials) .then(function (res) { Session.create(res.data.id, res.data.user.id, res.data.user.role); return res.data.user; }); }; authService.isAuthenticated = function () { return !!Session.userId; }; authService.isAuthorized = function (authorizedRoles) { if (!angular.isArray(authorizedRoles)) { authorizedRoles = [authorizedRoles]; } return (authService.isAuthenticated() && authorizedRoles.indexOf(Session.userRole) !== -1); }; return authService; })
Afin de m'éloigner davantage des préoccupations d'authentification d'identité, j'utilise un autre service (un objet singleton, utilisant le style service) pour sauvegarder les informations de session de l'utilisateur. Les détails des informations de session dépendent de l'implémentation du backend, mais je vais donner un exemple plus général :
.service('Session', function () { this.create = function (sessionId, userId, userRole) { this.id = sessionId; this.userId = userId; this.userRole = userRole; }; this.destroy = function () { this.id = null; this.userId = null; this.userRole = null; }; return this; })
Une fois l'utilisateur connecté, ses informations doivent être affichées à certains endroits (comme l'avatar de l'utilisateur ou quelque chose du genre dans le coin supérieur droit). Pour y parvenir, l'objet utilisateur doit être référencé par l'objet $scope, de préférence un objet qui peut être appelé globalement. Bien que $rootScope soit le premier choix évident, j'essaie de m'empêcher d'utiliser trop $rootScope (en fait, je n'utilise $rootScope que pour les diffusions d'événements globaux). La façon dont je préfère procéder est de définir un contrôleur au niveau du nœud racine de l'application, ou ailleurs au moins au-dessus de l'arborescence DOM. Les balises sont un bon choix :
<body ng-controller="ApplicationController"> ... </body>
ApplicationController est un conteneur pour la logique globale de l'application et une option pour exécuter la méthode run d'Angular. Par conséquent, il sera à la racine de l’arborescence $scope, et toutes les autres étendues en hériteront (à l’exception de la portée d’isolation). C'est un bon endroit pour définir l'objet currentUser :
.controller('ApplicationController', function ($scope, USER_ROLES, AuthService) { $scope.currentUser = null; $scope.userRoles = USER_ROLES; $scope.isAuthorized = AuthService.isAuthorized; $scope.setCurrentUser = function (user) { $scope.currentUser = user; }; })
Nous n'attribuons pas réellement l'objet currentUser, nous initialisons simplement les propriétés de portée afin que currentUser soit accessible ultérieurement. Malheureusement, nous ne pouvons pas simplement attribuer une nouvelle valeur à currentUser dans la portée enfant car cela créerait une propriété shadow. C'est le résultat du passage de types primitifs (chaînes, nombres, booléens, non définis et nuls) par valeur plutôt que par référence. Pour empêcher les propriétés d'ombre, nous devons utiliser des fonctions de définition. Si vous souhaitez en savoir plus sur les portées angulaires et l'héritage prototypique, lisez Comprendre les portées.
Contrôle d'accès
L'authentification d'identité, c'est-à-dire le contrôle d'accès, n'existe pas réellement dans AngularJS. Parce que nous sommes une application client, tout le code source est entre les mains de l'utilisateur. Il n'existe aucun moyen d'empêcher les utilisateurs de falsifier le code pour obtenir une interface authentifiée. Tout ce que nous pouvons faire, c'est montrer les commandes. Si vous avez besoin d'une véritable authentification, vous devrez le faire côté serveur, mais cela dépasse le cadre de cet article.
Restreindre l'affichage des éléments
AngularJS a des directives pour contrôler l'affichage ou le masquage des éléments en fonction de la portée ou des expressions : ngShow, ngHide, ngIf et ngSwitch. Les deux premiers masqueront l'élément à l'aide d'un attribut