AngularJS 應用身份認證的技巧總結_AngularJS

來源:互聯網
上載者:User

在web中很多時候都能應用到身份認證,本文介紹了AngularJS 應用身份認證的技巧,廢話不多說了一起往下看吧。

身份認證

最普遍的身份認證方式就是用使用者名稱(或 email)和密碼做登陸操作。這就意味要實現一個登陸的表單,以便使用者能夠用他們個人資訊登陸。這個表單看起來是這樣的:

<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>

既然這個是 Angular-powered 的表單,我們使用 ngSubmit 指令去觸發上傳表單時的函數。注意一點的是,我們把個人資訊傳入到上傳表單的函數,而不是直接使用 $scope.credentials 這個對象。這樣使得函數更容易進行 unit-test 和降低這個函數與當前 Controller 範圍的耦合。這個 Controller 看起來是這樣的:

.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);})

我們注意到這裡是缺少實際的邏輯的。這個 Controller 被做成這樣,目的是使身份認證的邏輯跟表單解耦。把邏輯儘可能的從我們的 Controller 裡面抽離出來,把他們都放到 services 裡面,這是個很好的想法。AngularJS 的 Controller 應該只管理 $scope 裡面的對象(用 watching 或者 手動操作)而不是承擔過多過分重的東西。

通知 Session 的變化

身份認證會影響整個應用的狀態。基於這個原因我更推薦使用事件(用 $broadcast)去通知 user session 的改變。把所有可能用到的事件代碼定義在一個中間地帶是個不錯的選擇。我喜歡用 constants 去做這個事情:

.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'})

constants 有個很好的特性就是他們能隨便注入到別的地方,就像 services 那樣。這樣使得 constants 很容易被我們的 unit-test 調用。constants 也允許你很容易地在隨後對他們重新命名而不需要改一大串檔案。同樣的戲法運用到了 user roles:

.constant('USER_ROLES', { all: '*', admin: 'admin', editor: 'editor', guest: 'guest'})

如果你想給予 editors 和 administrators 同樣的許可權,你只需要簡單地把 ‘editor' 改成 ‘admin'。

The AuthService

與身份認證和授權(存取控制)相關的邏輯最好被放到同一個 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;})

為了進一步遠離身份認證的擔憂,我使用另一個 service(一個單例對象,using the service style)去儲存使用者的 session 資訊。session 的資訊細節是依賴於後端的實現,但是我還是給出一個較普遍的例子吧:

.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;})

一旦使用者登入了,他的資訊應該會被展示在某些地方(比如右上方帳戶圖片什麼的)。為了實現這個,使用者物件必須要被 $scope 對象引用,更好的是一個可以被全域調用的地方。雖然 $rootScope 是顯然易見的第一個選擇,但是我嘗試克制自己,不過多地使用 $rootScope(實際上我只在全域事件廣播使用 $rootScope)。用我所喜歡的方式去做這個事情,就是在應用的根節點,或者在別的至少高於 Dom 樹的地方,定義一個 controller 。 標籤是個很好的選擇:

<body ng-controller="ApplicationController"> ...</body>

ApplicationController 是應用的全域邏輯的容器和一個用於運行 Angular 的 run 方法的選擇。因此它要處於 $scope 樹的根,所有其他的 scope 會繼承它(除了隔離 scope)。這是個很好的地方去定義 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; };})

我們實際上不分配 currentUser 對象,我們僅僅初始化範圍上的屬性以便 currentUser 能在後面被訪問到。不幸的是,我們不能簡單地在子範圍分配一個新的值給 currentUser 因為那樣會造成 shadow property。這是用以值傳遞原始類型(strings, numbers, booleans,undefined and null)代替以引用傳遞原始類型的結果。為了防止 shadow property,我們要使用 setter 函數。如果想瞭解更多 Angular 範圍和原形繼承,請閱讀 Understanding Scopes。

存取控制

身份認證,也就是存取控制,其實在 AngularJS 並不存在。因為我們是用戶端應用,所有源碼都在使用者手上。沒有辦法阻止使用者篡改代碼以獲得認證後的介面。我們能做的只是顯示控制。如果你需要真正的身份認證,你需要在伺服器端做這個事情,但是這個超出了本文範疇。

限制元素的顯示

AngularJS 擁有基於範圍或者運算式來控制顯示或者隱藏元素的指令: ngShow, ngHide, ngIf 和 ngSwitch。前兩個會使用一個 <style> 屬性去隱藏元素,但是後兩個會從 DOM 移除元素。

第一種方式,也就是隱藏元素,最好用於運算式頻繁改變並且沒有包含過多的模板邏輯和範圍引用的元素上。原因是在隱藏的元素裡,這些元素的模板邏輯仍然會在每個 digest 迴圈裡重新計算,使得應用效能下降。第二種方式,移除元素,也會移除所有在這個元素上的 handler 和範圍綁定。改變 DOM 對於瀏覽器來說是很大工作量的(在某些情境,和 ngShow/ngHide 對比),但是在很多時候這種代價是值得的。因為使用者訪問資訊不會經常改變,使用 ngIf 或 ngShow 是最好的選擇:

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

限制路由訪問

很多時候你會想讓整個網頁都不能被訪問,而不是僅僅隱藏一個元素。如果可以再路由(在UI Router 裡,路由也叫狀態)使用一種自訂的資料結構,我們就可以明確哪些使用者角色可以被允許訪問哪些內容。下面這個例子使用 UI Router 的風格,但是這些同樣適用於 ngRoute。

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

下一步,我們需要檢查每次路由變化(就是使用者跳轉到其他頁面的時候)。這需要監聽 $routeChangStart(ngRoute 裡的)或者 $stateChangeStart(UI Router 裡的)事件:

.run(function ($rootScope, AUTH_EVENTS, AuthService) { $rootScope.$on('$stateChangeStart', 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([  '$injector',  function ($injector) {   return $injector.get('AuthInterceptor');  } ]);}).factory('AuthInterceptor', 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('loginDialog', function (AUTH_EVENTS) { return {  restrict: 'A',  template: '<div ng-if="visible"          ng-include="\'login-form.html\'">',  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。你需要在每個頁面寫上如下的代碼:

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

注意 isLoginPage 檢查。一個失敗了的登陸會觸發 notAuthenticated 時間,但我們不想在登陸頁面顯示這個對話方塊,因為這很多餘和奇怪。這就是為什麼我們不把登陸對話方塊也放在登陸頁面的原因。所以在 ApplicationController 裡定義一個 $scope.isLoginPage 是合理的。

儲存使用者狀態

在使用者重新整理他們的頁面,依舊儲存已登陸的使用者資訊是單頁應用認證裡面狡猾的一個環節。因為所有狀態都存在用戶端,重新整理會清空使用者資訊。為了修複這個問題,我通常實現一個會返回已登陸的目前使用者的資料的 API (比如 /profile),這個 API 會在 AngularJS 應用啟動(比如在 “run” 函數)。然後使用者資料會被儲存在 Session 服務或者 $rootScope,就像使用者已經登陸後的狀態。或者,你可以把使用者資料直接嵌入到 index.html,這樣就不用額外的請求了。第三種方式就是把使用者資料存在 cookie 或者 LocalStorage,但這會使得登出或者清空使用者資料變得困難一點。

最後……

鄙人才疏學淺,一點點經驗,這是一篇翻譯的文章,如有謬誤,歡迎指正。

以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支援雲棲社區。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.