AngularJS 應(yīng)用身份認證的技巧總結(jié)
在web中很多時候都能應(yīng)用到身份認證,本文介紹了AngularJS 應(yīng)用身份認證的技巧,廢話不多說了一起往下看吧。
身份認證
最普遍的身份認證方式就是用用戶名(或 email)和密碼做登陸操作。這就意味要實現(xiàn)一個登陸的表單,以便用戶能夠用他們個人信息登陸。這個表單看起來是這樣的:
<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 指令去觸發(fā)上傳表單時的函數(shù)。注意一點的是,我們把個人信息傳入到上傳表單的函數(shù),而不是直接使用 $scope.credentials 這個對象。這樣使得函數(shù)更容易進行 unit-test 和降低這個函數(shù)與當(dāng)前 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 應(yīng)該只管理 $scope 里面的對象(用 watching 或者 手動操作)而不是承擔(dān)過多過分重的東西。
通知 Session 的變化
身份認證會影響整個應(yīng)用的狀態(tài)?;谶@個原因我更推薦使用事件(用 $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 調(diào)用。constants 也允許你很容易地在隨后對他們重命名而不需要改一大串文件。同樣的戲法運用到了 user roles:
.constant('USER_ROLES', { all: '*', admin: 'admin', editor: 'editor', guest: 'guest' })
如果你想給予 editors 和 administrators 同樣的權(quán)限,你只需要簡單地把 ‘editor' 改成 ‘a(chǎn)dmin'。
The AuthService
與身份認證和授權(quán)(訪問控制)相關(guān)的邏輯最好被放到同一個 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; })
為了進一步遠離身份認證的擔(dān)憂,我使用另一個 service(一個單例對象,using the service style)去保存用戶的 session 信息。session 的信息細節(jié)是依賴于后端的實現(xiàn),但是我還是給出一個較普遍的例子吧:
.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; })
一旦用戶登錄了,他的信息應(yīng)該會被展示在某些地方(比如右上角用戶頭像什么的)。為了實現(xiàn)這個,用戶對象必須要被 $scope 對象引用,更好的是一個可以被全局調(diào)用的地方。雖然 $rootScope 是顯然易見的第一個選擇,但是我嘗試克制自己,不過多地使用 $rootScope(實際上我只在全局事件廣播使用 $rootScope)。用我所喜歡的方式去做這個事情,就是在應(yīng)用的根節(jié)點,或者在別的至少高于 Dom 樹的地方,定義一個 controller 。 標簽是個很好的選擇:
<body ng-controller="ApplicationController"> ... </body>
ApplicationController 是應(yīng)用的全局邏輯的容器和一個用于運行 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)代替以引用傳遞原始類型的結(jié)果。為了防止 shadow property,我們要使用 setter 函數(shù)。如果想了解更多 Angular 作用域和原形繼承,請閱讀 Understanding Scopes。
訪問控制
身份認證,也就是訪問控制,其實在 AngularJS 并不存在。因為我們是客戶端應(yīng)用,所有源碼都在用戶手上。沒有辦法阻止用戶篡改代碼以獲得認證后的界面。我們能做的只是顯示控制。如果你需要真正的身份認證,你需要在服務(wù)器端做這個事情,但是這個超出了本文范疇。
限制元素的顯示
AngularJS 擁有基于作用域或者表達式來控制顯示或者隱藏元素的指令: ngShow, ngHide, ngIf 和 ngSwitch。前兩個會使用一個 <style> 屬性去隱藏元素,但是后兩個會從 DOM 移除元素。
第一種方式,也就是隱藏元素,最好用于表達式頻繁改變并且沒有包含過多的模板邏輯和作用域引用的元素上。原因是在隱藏的元素里,這些元素的模板邏輯仍然會在每個 digest 循環(huán)里重新計算,使得應(yīng)用性能下降。第二種方式,移除元素,也會移除所有在這個元素上的 handler 和作用域綁定。改變 DOM 對于瀏覽器來說是很大工作量的(在某些場景,和 ngShow/ngHide 對比),但是在很多時候這種代價是值得的。因為用戶訪問信息不會經(jīng)常改變,使用 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>
限制路由訪問
很多時候你會想讓整個網(wǎng)頁都不能被訪問,而不是僅僅隱藏一個元素。如果可以再路由(在UI Router 里,路由也叫狀態(tài))使用一種自定義的數(shù)據(jù)結(jié)構(gòu),我們就可以明確哪些用戶角色可以被允許訪問哪些內(nèi)容。下面這個例子使用 UI Router 的風(fēng)格,但是這些同樣適用于 ngRoute。
.config(function ($stateProvider, USER_ROLES) { $stateProvider.state('dashboard', { url: '/dashboard', templateUrl: 'dashboard/index.html', data: { authorizedRoles: [USER_ROLES.admin, USER_ROLES.editor] } }); })
下一步,我們需要檢查每次路由變化(就是用戶跳轉(zhuǎn)到其他頁面的時候)。這需要監(jiān)聽 $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 時效
身份認證多半是服務(wù)器端的事情。無論你用什么實現(xiàn)方式,你的后端會對用戶信息做真正的驗證和處理諸如 Session 時效和訪問控制的處理。這意味著你的 API 會有時返回一些認證錯誤。標準的錯誤碼就是 HTTP 狀態(tài)嗎。普遍使用這些錯誤碼:
- 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
后兩種不是標準內(nèi)容,但是可能廣泛應(yīng)用。最好的官方的判斷 session 過期的錯誤碼是 401。無論怎樣,你的登陸對話框都應(yīng)該在 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); } }; })
這只是一個認證攔截器的簡單實現(xiàn)。有個很棒的項目在 Github ,它做了相同的事情,并且使用了 httpBuffer 服務(wù)。當(dāng)返回 HTTP 錯誤碼時,它會阻止用戶進一步的請求,直到用戶再次登錄,然后繼續(xù)這個請求。
登錄對話框指令
當(dāng)一個 session 過期了,我們需要用戶重新進入他的賬號。為了防止他丟失他當(dāng)前的工作,最好的方法就是彈出登錄登錄對話框,而不是跳轉(zhuǎn)到登錄頁面。這個對話框需要監(jiān)聽 notAuthenticated 和 sessionTimeout 事件,所以當(dāng)其中一個事件被觸發(fā)了,對話框就要打開:
.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 檢查。一個失敗了的登陸會觸發(fā) notAuthenticated 時間,但我們不想在登陸頁面顯示這個對話框,因為這很多余和奇怪。這就是為什么我們不把登陸對話框也放在登陸頁面的原因。所以在 ApplicationController 里定義一個 $scope.isLoginPage 是合理的。
保存用戶狀態(tài)
在用戶刷新他們的頁面,依舊保存已登陸的用戶信息是單頁應(yīng)用認證里面狡猾的一個環(huán)節(jié)。因為所有狀態(tài)都存在客戶端,刷新會清空用戶信息。為了修復(fù)這個問題,我通常實現(xiàn)一個會返回已登陸的當(dāng)前用戶的數(shù)據(jù)的 API (比如 /profile),這個 API 會在 AngularJS 應(yīng)用啟動(比如在 “run” 函數(shù))。然后用戶數(shù)據(jù)會被保存在 Session 服務(wù)或者 $rootScope,就像用戶已經(jīng)登陸后的狀態(tài)?;蛘?,你可以把用戶數(shù)據(jù)直接嵌入到 index.html,這樣就不用額外的請求了。第三種方式就是把用戶數(shù)據(jù)存在 cookie 或者 LocalStorage,但這會使得登出或者清空用戶數(shù)據(jù)變得困難一點。
最后……
鄙人才疏學(xué)淺,一點點經(jīng)驗,這是一篇翻譯的文章,如有謬誤,歡迎指正。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Angular 2父子組件之間共享服務(wù)通信的實現(xiàn)
這篇文章主要給大家介紹了關(guān)于Angular 2父子組件之間共享服務(wù)通信的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-07-07用AngularJS來實現(xiàn)監(jiān)察表單按鈕的禁用效果
本篇文章主要介紹了用AngularJS來實現(xiàn)監(jiān)察表單按鈕的禁用效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2016-11-11淺談angularjs $http提交數(shù)據(jù)探索
這篇文章主要介紹了淺談angularjs $http提交數(shù)據(jù)探索,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-01-01Angular 1.x個人使用的經(jīng)驗小結(jié)
這篇文章主要給大家介紹了關(guān)于Angular 1.x個人使用的一些經(jīng)驗,屬于一些基礎(chǔ)入門教程,文中通過示例代碼介紹的非常詳細,對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-07-07Commands Queries設(shè)計模式提高Angular應(yīng)用性能及可維護性
在Angular應(yīng)用開發(fā)領(lǐng)域,Commands and Queries 設(shè)計模式是一個關(guān)鍵的概念,它有助于有效地管理應(yīng)用程序的狀態(tài)和與后端的交互,本文將深入探討這一設(shè)計模式的核心要點,并通過實際示例來加以說明2023-10-10Angular2使用SVG自定義圖表(條形圖、折線圖)組件示例
這篇文章主要介紹了Angular2使用SVG自定義圖表(條形圖、折線圖)組件,涉及Angular結(jié)合svg進行圖表繪制的相關(guān)操作技巧,需要的朋友可以參考下2019-05-05