AngularJS 應(yīng)用身份認(rèn)證的技巧總結(jié)
在web中很多時(shí)候都能應(yīng)用到身份認(rèn)證,本文介紹了AngularJS 應(yīng)用身份認(rèn)證的技巧,廢話不多說(shuō)了一起往下看吧。
身份認(rèn)證
最普遍的身份認(rèn)證方式就是用用戶名(或 email)和密碼做登陸操作。這就意味要實(shí)現(xiàn)一個(gè)登陸的表單,以便用戶能夠用他們個(gè)人信息登陸。這個(gè)表單看起來(lái)是這樣的:
<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>
既然這個(gè)是 Angular-powered 的表單,我們使用 ngSubmit 指令去觸發(fā)上傳表單時(shí)的函數(shù)。注意一點(diǎn)的是,我們把個(gè)人信息傳入到上傳表單的函數(shù),而不是直接使用 $scope.credentials 這個(gè)對(duì)象。這樣使得函數(shù)更容易進(jìn)行 unit-test 和降低這個(gè)函數(shù)與當(dāng)前 Controller 作用域的耦合。這個(gè) Controller 看起來(lái)是這樣的:
.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);
})
我們注意到這里是缺少實(shí)際的邏輯的。這個(gè) Controller 被做成這樣,目的是使身份認(rèn)證的邏輯跟表單解耦。把邏輯盡可能的從我們的 Controller 里面抽離出來(lái),把他們都放到 services 里面,這是個(gè)很好的想法。AngularJS 的 Controller 應(yīng)該只管理 $scope 里面的對(duì)象(用 watching 或者 手動(dòng)操作)而不是承擔(dān)過多過分重的東西。
通知 Session 的變化
身份認(rèn)證會(huì)影響整個(gè)應(yīng)用的狀態(tài)?;谶@個(gè)原因我更推薦使用事件(用 $broadcast)去通知 user session 的改變。把所有可能用到的事件代碼定義在一個(gè)中間地帶是個(gè)不錯(cuò)的選擇。我喜歡用 constants 去做這個(gè)事情:
.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 有個(gè)很好的特性就是他們能隨便注入到別的地方,就像 services 那樣。這樣使得 constants 很容易被我們的 unit-test 調(diào)用。constants 也允許你很容易地在隨后對(duì)他們重命名而不需要改一大串文件。同樣的戲法運(yùn)用到了 user roles:
.constant('USER_ROLES', {
all: '*',
admin: 'admin',
editor: 'editor',
guest: 'guest'
})
如果你想給予 editors 和 administrators 同樣的權(quán)限,你只需要簡(jiǎn)單地把 ‘editor' 改成 ‘a(chǎn)dmin'。
The AuthService
與身份認(rèn)證和授權(quán)(訪問控制)相關(guān)的邏輯最好被放到同一個(gè) 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;
})
為了進(jìn)一步遠(yuǎn)離身份認(rèn)證的擔(dān)憂,我使用另一個(gè) service(一個(gè)單例對(duì)象,using the service style)去保存用戶的 session 信息。session 的信息細(xì)節(jié)是依賴于后端的實(shí)現(xiàn),但是我還是給出一個(gè)較普遍的例子吧:
.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)該會(huì)被展示在某些地方(比如右上角用戶頭像什么的)。為了實(shí)現(xiàn)這個(gè),用戶對(duì)象必須要被 $scope 對(duì)象引用,更好的是一個(gè)可以被全局調(diào)用的地方。雖然 $rootScope 是顯然易見的第一個(gè)選擇,但是我嘗試克制自己,不過多地使用 $rootScope(實(shí)際上我只在全局事件廣播使用 $rootScope)。用我所喜歡的方式去做這個(gè)事情,就是在應(yīng)用的根節(jié)點(diǎn),或者在別的至少高于 Dom 樹的地方,定義一個(gè) controller 。 標(biāo)簽是個(gè)很好的選擇:
<body ng-controller="ApplicationController"> ... </body>
ApplicationController 是應(yīng)用的全局邏輯的容器和一個(gè)用于運(yùn)行 Angular 的 run 方法的選擇。因此它要處于 $scope 樹的根,所有其他的 scope 會(huì)繼承它(除了隔離 scope)。這是個(gè)很好的地方去定義 currentUser 對(duì)象:
.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;
};
})
我們實(shí)際上不分配 currentUser 對(duì)象,我們僅僅初始化作用域上的屬性以便 currentUser 能在后面被訪問到。不幸的是,我們不能簡(jiǎn)單地在子作用域分配一個(gè)新的值給 currentUser 因?yàn)槟菢訒?huì)造成 shadow property。這是用以值傳遞原始類型(strings, numbers, booleans,undefined and null)代替以引用傳遞原始類型的結(jié)果。為了防止 shadow property,我們要使用 setter 函數(shù)。如果想了解更多 Angular 作用域和原形繼承,請(qǐng)閱讀 Understanding Scopes。
訪問控制
身份認(rèn)證,也就是訪問控制,其實(shí)在 AngularJS 并不存在。因?yàn)槲覀兪强蛻舳藨?yīng)用,所有源碼都在用戶手上。沒有辦法阻止用戶篡改代碼以獲得認(rèn)證后的界面。我們能做的只是顯示控制。如果你需要真正的身份認(rèn)證,你需要在服務(wù)器端做這個(gè)事情,但是這個(gè)超出了本文范疇。
限制元素的顯示
AngularJS 擁有基于作用域或者表達(dá)式來(lái)控制顯示或者隱藏元素的指令: ngShow, ngHide, ngIf 和 ngSwitch。前兩個(gè)會(huì)使用一個(gè) <style> 屬性去隱藏元素,但是后兩個(gè)會(huì)從 DOM 移除元素。
第一種方式,也就是隱藏元素,最好用于表達(dá)式頻繁改變并且沒有包含過多的模板邏輯和作用域引用的元素上。原因是在隱藏的元素里,這些元素的模板邏輯仍然會(huì)在每個(gè) digest 循環(huán)里重新計(jì)算,使得應(yīng)用性能下降。第二種方式,移除元素,也會(huì)移除所有在這個(gè)元素上的 handler 和作用域綁定。改變 DOM 對(duì)于瀏覽器來(lái)說(shuō)是很大工作量的(在某些場(chǎng)景,和 ngShow/ngHide 對(duì)比),但是在很多時(shí)候這種代價(jià)是值得的。因?yàn)橛脩粼L問信息不會(huì)經(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>
限制路由訪問
很多時(shí)候你會(huì)想讓整個(gè)網(wǎng)頁(yè)都不能被訪問,而不是僅僅隱藏一個(gè)元素。如果可以再路由(在UI Router 里,路由也叫狀態(tài))使用一種自定義的數(shù)據(jù)結(jié)構(gòu),我們就可以明確哪些用戶角色可以被允許訪問哪些內(nèi)容。下面這個(gè)例子使用 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)到其他頁(yè)面的時(shí)候)。這需要監(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 時(shí)效
身份認(rèn)證多半是服務(wù)器端的事情。無(wú)論你用什么實(shí)現(xiàn)方式,你的后端會(huì)對(duì)用戶信息做真正的驗(yàn)證和處理諸如 Session 時(shí)效和訪問控制的處理。這意味著你的 API 會(huì)有時(shí)返回一些認(rèn)證錯(cuò)誤。標(biāo)準(zhǔn)的錯(cuò)誤碼就是 HTTP 狀態(tài)嗎。普遍使用這些錯(cuò)誤碼:
- 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
后兩種不是標(biāo)準(zhǔn)內(nèi)容,但是可能廣泛應(yīng)用。最好的官方的判斷 session 過期的錯(cuò)誤碼是 401。無(wú)論怎樣,你的登陸對(duì)話框都應(yīng)該在 API 返回 401, 419, 440 或者 403 的時(shí)候馬上顯示出來(lái)??偟膩?lái)說(shuō),我們想廣播和基于這些 HTTP 返回碼的時(shí)間,為此我們?cè)?$httpProvider 增加一個(gè)攔截器:
.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);
}
};
})
這只是一個(gè)認(rèn)證攔截器的簡(jiǎn)單實(shí)現(xiàn)。有個(gè)很棒的項(xiàng)目在 Github ,它做了相同的事情,并且使用了 httpBuffer 服務(wù)。當(dāng)返回 HTTP 錯(cuò)誤碼時(shí),它會(huì)阻止用戶進(jìn)一步的請(qǐng)求,直到用戶再次登錄,然后繼續(xù)這個(gè)請(qǐng)求。
登錄對(duì)話框指令
當(dāng)一個(gè) session 過期了,我們需要用戶重新進(jìn)入他的賬號(hào)。為了防止他丟失他當(dāng)前的工作,最好的方法就是彈出登錄登錄對(duì)話框,而不是跳轉(zhuǎn)到登錄頁(yè)面。這個(gè)對(duì)話框需要監(jiān)聽 notAuthenticated 和 sessionTimeout 事件,所以當(dāng)其中一個(gè)事件被觸發(fā)了,對(duì)話框就要打開:
.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)
}
};
})
只要你喜歡,這個(gè)對(duì)話框可以隨便擴(kuò)展。主要的思想是重用已存在的登陸表單模板和 LoginController。你需要在每個(gè)頁(yè)面寫上如下的代碼:
<div login-dialog ng-if="!isLoginPage"></div>
注意 isLoginPage 檢查。一個(gè)失敗了的登陸會(huì)觸發(fā) notAuthenticated 時(shí)間,但我們不想在登陸頁(yè)面顯示這個(gè)對(duì)話框,因?yàn)檫@很多余和奇怪。這就是為什么我們不把登陸對(duì)話框也放在登陸頁(yè)面的原因。所以在 ApplicationController 里定義一個(gè) $scope.isLoginPage 是合理的。
保存用戶狀態(tài)
在用戶刷新他們的頁(yè)面,依舊保存已登陸的用戶信息是單頁(yè)應(yīng)用認(rèn)證里面狡猾的一個(gè)環(huán)節(jié)。因?yàn)樗袪顟B(tài)都存在客戶端,刷新會(huì)清空用戶信息。為了修復(fù)這個(gè)問題,我通常實(shí)現(xiàn)一個(gè)會(huì)返回已登陸的當(dāng)前用戶的數(shù)據(jù)的 API (比如 /profile),這個(gè) API 會(huì)在 AngularJS 應(yīng)用啟動(dòng)(比如在 “run” 函數(shù))。然后用戶數(shù)據(jù)會(huì)被保存在 Session 服務(wù)或者 $rootScope,就像用戶已經(jīng)登陸后的狀態(tài)?;蛘撸憧梢园延脩魯?shù)據(jù)直接嵌入到 index.html,這樣就不用額外的請(qǐng)求了。第三種方式就是把用戶數(shù)據(jù)存在 cookie 或者 LocalStorage,但這會(huì)使得登出或者清空用戶數(shù)據(jù)變得困難一點(diǎn)。
最后……
鄙人才疏學(xué)淺,一點(diǎn)點(diǎn)經(jīng)驗(yàn),這是一篇翻譯的文章,如有謬誤,歡迎指正。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Angular將填入表單的數(shù)據(jù)渲染到表格的方法
這篇文章主要介紹了Angular將填入表單的數(shù)據(jù)渲染到表格的方法,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-09-09
Angularjs實(shí)現(xiàn)搜索關(guān)鍵字高亮顯示效果
本篇文章主要介紹了Angularjs實(shí)現(xiàn)搜索關(guān)鍵字高亮顯示的方法,具有一定的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-01-01
詳解ng-alain動(dòng)態(tài)表單SF表單項(xiàng)設(shè)置必填和正則校驗(yàn)
這篇文章主要介紹了詳解ng-alain動(dòng)態(tài)表單SF表單項(xiàng)設(shè)置必填和正則校驗(yàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2019-06-06
Angular2關(guān)于@angular/cli默認(rèn)端口號(hào)配置的問題
本篇文章主要介紹了Angular2關(guān)于@angular/cli默認(rèn)端口號(hào)配置的問題,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-07-07
AngularJS實(shí)現(xiàn)表單元素值綁定操作示例
這篇文章主要介紹了AngularJS實(shí)現(xiàn)表單元素值綁定操作,結(jié)合具體實(shí)例形式分析了AngularJS針對(duì)表單元素屬性相關(guān)操作技巧,需要的朋友可以參考下2017-10-10
AngularJS實(shí)現(xiàn)樹形結(jié)構(gòu)(ztree)菜單示例代碼
這篇文章運(yùn)用示例代碼給大家詳細(xì)介紹了利用AngularJS如何實(shí)現(xiàn)樹形結(jié)構(gòu)(ztree)菜單,文中僅用了幾行AngularJS代碼就是了這個(gè)功能,對(duì)大家日常開發(fā)很有幫助,有需要的朋友們可以參考借鑒,下面來(lái)一起看看吧。2016-09-09
AngularJs學(xué)習(xí)第八篇 過濾器filter創(chuàng)建
這篇文章主要介紹了AngularJs學(xué)習(xí)第八篇 過濾器filter創(chuàng)建的相關(guān)資料,非常不錯(cuò)具有參考借鑒價(jià)值,需要的朋友可以參考下2016-06-06
使用ionic播放輪詢廣告的實(shí)現(xiàn)方法(必看)
下面小編就為大家?guī)?lái)一篇使用ionic播放輪詢廣告的實(shí)現(xiàn)方法(必看)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2017-04-04

