身份認證
最普遍的身份認證方式就是用使用者名稱(或 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。前兩者會使用一個