"/> ">

Maison  >  Article  >  interface Web  >  Exemple de code détaillé sur la façon d'utiliser AngularJS pour l'authentification

Exemple de code détaillé sur la façon d'utiliser AngularJS pour l'authentification

伊谢尔伦
伊谢尔伦original
2017-07-20 10:46:101509parcourir

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(&#39;LoginController&#39;, function ($scope, $rootScope, AUTH_EVENTS, AuthService) {
 $scope.credentials = {
  username: &#39;&#39;,
  password: &#39;&#39;
 };
 $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(&#39;AUTH_EVENTS&#39;, {
 loginSuccess: &#39;auth-login-success&#39;,
 loginFailed: &#39;auth-login-failed&#39;,
 logoutSuccess: &#39;auth-logout-success&#39;,
 sessionTimeout: &#39;auth-session-timeout&#39;,
 notAuthenticated: &#39;auth-not-authenticated&#39;,
 notAuthorized: &#39;auth-not-authorized&#39;
})

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(&#39;USER_ROLES&#39;, {
 all: &#39;*&#39;,
 admin: &#39;admin&#39;,
 editor: &#39;editor&#39;,
 guest: &#39;guest&#39;
})

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(&#39;AuthService&#39;, function ($http, Session) {
 var authService = {};

 authService.login = function (credentials) {
  return $http
   .post(&#39;/login&#39;, 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(&#39;Session&#39;, 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(&#39;ApplicationController&#39;, 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 c9ccee2e6ea535a969eb3f532ad9fe89, mais les deux derniers supprimeront l'élément du DOM.

第一种方式,也就是隐藏元素,最好用于表达式频繁改变并且没有包含过多的模板逻辑和作用域引用的元素上。原因是在隐藏的元素里,这些元素的模板逻辑仍然会在每个 digest 循环里重新计算,使得应用性能下降。第二种方式,移除元素,也会移除所有在这个元素上的 handler 和作用域绑定。改变 DOM 对于浏览器来说是很大工作量的(在某些场景,和 ngShow/ngHide 对比),但是在很多时候这种代价是值得的。因为用户访问信息不会经常改变,使用 ngIf 或 ngShow 是最好的选择:

<p ng-if="currentUser">Welcome, {{ currentUser.name }}</p>
<p ng-if="isAuthorized(userRoles.admin)">You&#39;re admin.</p>
<p ng-switch on="currentUser.role">
 <p ng-switch-when="userRoles.admin">You&#39;re admin.</p>
 <p ng-switch-when="userRoles.editor">You&#39;re editor.</p>
 <p ng-switch-default>You&#39;re something else.</p>
</p>

限制路由访问

很多时候你会想让整个网页都不能被访问,而不是仅仅隐藏一个元素。如果可以再路由(在UI Router 里,路由也叫状态)使用一种自定义的数据结构,我们就可以明确哪些用户角色可以被允许访问哪些内容。下面这个例子使用 UI Router 的风格,但是这些同样适用于 ngRoute。

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

下一步,我们需要检查每次路由变化(就是用户跳转到其他页面的时候)。这需要监听 $routeChangStart(ngRoute 里的)或者 $stateChangeStart(UI Router 里的)事件:

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

Session 时效

身份认证多半是服务器端的事情。无论你用什么实现方式,你的后端会对用户信息做真正的验证和处理诸如 Session 时效和访问控制的处理。这意味着你的 API 会有时返回一些认证错误。标准的错误码就是 HTTP 状态吗。普遍使用这些错误码:

  • 401 Unauthorized — The user is not logged in

  • 403 Forbidden — The user is logged in but isn't allowed access

  • 419 Authentication Timeout (non standard) — Session has expired

  • 440 Login Timeout (Microsoft only) — Session has expired

后两种不是标准内容,但是可能广泛应用。最好的官方的判断 session 过期的错误码是 401。无论怎样,你的登陆对话框都应该在 API 返回 401, 419, 440 或者 403 的时候马上显示出来。总的来说,我们想广播和基于这些 HTTP 返回码的时间,为此我们在 $httpProvider 增加一个拦截器:

.config(function ($httpProvider) {
 $httpProvider.interceptors.push([
  &#39;$injector&#39;,
  function ($injector) {
   return $injector.get(&#39;AuthInterceptor&#39;);
  }
 ]);
})
.factory(&#39;AuthInterceptor&#39;, function ($rootScope, $q,
                   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);
  }
 };
})

这只是一个认证拦截器的简单实现。有个很棒的项目在 Github ,它做了相同的事情,并且使用了 httpBuffer 服务。当返回 HTTP 错误码时,它会阻止用户进一步的请求,直到用户再次登录,然后继续这个请求。

登录对话框指令

当一个 session 过期了,我们需要用户重新进入他的账号。为了防止他丢失他当前的工作,最好的方法就是弹出登录登录对话框,而不是跳转到登录页面。这个对话框需要监听 notAuthenticated 和 sessionTimeout 事件,所以当其中一个事件被触发了,对话框就要打开:

.directive(&#39;loginDialog&#39;, function (AUTH_EVENTS) {
 return {
  restrict: &#39;A&#39;,
  template: &#39;<p ng-if="visible"
          ng-include="\&#39;login-form.html\&#39;">&#39;,
  link: function (scope) {
   var showDialog = function () {
    scope.visible = true;
   };

   scope.visible = false;
   scope.$on(AUTH_EVENTS.notAuthenticated, showDialog);
   scope.$on(AUTH_EVENTS.sessionTimeout, showDialog)
  }
 };
})

只要你喜欢,这个对话框可以随便扩展。主要的思想是重用已存在的登陆表单模板和 LoginController。你需要在每个页面写上如下的代码:

<p login-dialog ng-if="!isLoginPage"></p>

注意 isLoginPage 检查。一个失败了的登陆会触发 notAuthenticated 时间,但我们不想在登陆页面显示这个对话框,因为这很多余和奇怪。这就是为什么我们不把登陆对话框也放在登陆页面的原因。所以在 ApplicationController 里定义一个 $scope.isLoginPage 是合理的。

保存用户状态

在用户刷新他们的页面,依旧保存已登陆的用户信息是单页应用认证里面狡猾的一个环节。因为所有状态都存在客户端,刷新会清空用户信息。为了修复这个问题,我通常实现一个会返回已登陆的当前用户的数据的 API (比如 /profile),这个 API 会在 AngularJS 应用启动(比如在 “run” 函数)。然后用户数据会被保存在 Session 服务或者 $rootScope,就像用户已经登陆后的状态。或者,你可以把用户数据直接嵌入到 index.html,这样就不用额外的请求了。第三种方式就是把用户数据存在 cookie 或者 LocalStorage,但这会使得登出或者清空用户数据变得困难一点。

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn