"/> ">
身份認證
最普遍的身份認證方式就是用使用者名稱(或 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。前兩者會使用一個 c9ccee2e6ea535a969eb3f532ad9fe89 屬性去隱藏元素,但後兩個會從 DOM 移除元素。
第一种方式,也就是隐藏元素,最好用于表达式频繁改变并且没有包含过多的模板逻辑和作用域引用的元素上。原因是在隐藏的元素里,这些元素的模板逻辑仍然会在每个 digest 循环里重新计算,使得应用性能下降。第二种方式,移除元素,也会移除所有在这个元素上的 handler 和作用域绑定。改变 DOM 对于浏览器来说是很大工作量的(在某些场景,和 ngShow/ngHide 对比),但是在很多时候这种代价是值得的。因为用户访问信息不会经常改变,使用 ngIf 或 ngShow 是最好的选择:
<p ng-if="currentUser">Welcome, {{ currentUser.name }}</p> <p ng-if="isAuthorized(userRoles.admin)">You're admin.</p> <p ng-switch on="currentUser.role"> <p ng-switch-when="userRoles.admin">You're admin.</p> <p ng-switch-when="userRoles.editor">You're editor.</p> <p ng-switch-default>You're something else.</p> </p>
限制路由访问
很多时候你会想让整个网页都不能被访问,而不是仅仅隐藏一个元素。如果可以再路由(在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: '<p 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。你需要在每个页面写上如下的代码:
<p login-dialog ng-if="!isLoginPage"></p>
注意 isLoginPage 检查。一个失败了的登陆会触发 notAuthenticated 时间,但我们不想在登陆页面显示这个对话框,因为这很多余和奇怪。这就是为什么我们不把登陆对话框也放在登陆页面的原因。所以在 ApplicationController 里定义一个 $scope.isLoginPage 是合理的。
保存用户状态
在用户刷新他们的页面,依旧保存已登陆的用户信息是单页应用认证里面狡猾的一个环节。因为所有状态都存在客户端,刷新会清空用户信息。为了修复这个问题,我通常实现一个会返回已登陆的当前用户的数据的 API (比如 /profile),这个 API 会在 AngularJS 应用启动(比如在 “run” 函数)。然后用户数据会被保存在 Session 服务或者 $rootScope,就像用户已经登陆后的状态。或者,你可以把用户数据直接嵌入到 index.html,这样就不用额外的请求了。第三种方式就是把用户数据存在 cookie 或者 LocalStorage,但这会使得登出或者清空用户数据变得困难一点。
以上是如何使用AngularJS進行身份驗證的程式碼實例詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!