AngularJs 登錄的簡單實現
多數AngularJs應用離不開登錄操作,最近閱讀了一篇關於AngularJs登錄的博客,博客中實現的登錄系統demo能夠應用於多數小型AngularJs應用,實現也並不困難,這里講講如何實現這個簡單的登錄系統。
種子項目
這里使用的種子項目是 angular-seed
,登錄系統會在這個種子項目的基礎上完成
,github地址:https://github.com/angular/angular-seed/。按照github上README.md
配置后便可在上面添加我們自己的登錄系統。
angular-seed文件目錄:
app/ --> all of the source files for the application
app.css --> default stylesheet
components/ --> all app specific modules
version/ --> version related components
version.js --> version module declaration and basic "version" value service
version_test.js --> "version" value service tests
version-directive.js --> custom directive that returns the current app version
version-directive_test.js --> version directive tests
interpolate-filter.js --> custom interpolation filter interpolate-filter_test.js --> interpolate filter tests
view1/ --> the view1 view template and logic
view1.html --> the partial template
view1.js --> the controller logic
view1_test.js --> tests of the controller
view2/ --> the view2 view template and logic
view2.html --> the partial template
view2.js --> the controller logic
view2_test.js --> tests of the controller
app.js --> main application module
index.html --> app layout file (the main html template file of the app)
index-async.html --> just like index.html, but loads js files asynchronously
karma.conf.js --> config file for running unit tests with Karma
e2e-tests/ --> end-to-end tests
protractor-conf.js --> Protractor config file
scenarios.js --> end-to-end scenarios to be run by Protractor
這里,主要修改app.js以及view1文件夾相關文件,其中,view1將作為登錄界面。
具體實現
實現登錄表單
一個簡單實用的登錄表單的html文件:
<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>
將該表單代碼放入view1.html
中,並且修改view1.js
為該表單添加對應的controller,即LoginController
.如下:
// controller
.controller('LoginController', function($scope, $rootScope, AUTH_EVENTS, AuthService) {
$scope.credentials = {
username : '',
password : ''
};
$scope.login = function(credentials) {
console.log('login', credentials);
AuthService.login(credentials).then(function(user) {
$rootScope.$broadcast(AUTH_EVENTS.loginSuccess);
$scope.$parent.setCurrentUser(user);
}, function() {
$rootScope.$broadcast(AUTH_EVENTS.loginFailed);
});
};
})
這里的credentials存放用戶信息,值得注意的是:這里$scope.login
僅完成抽象邏輯,具體的邏輯實現依靠AuthService
這樣的service,在controller
里面建議多使用抽象邏輯,而非具體的實現。
用戶登錄狀態記錄
通常,用戶的登錄情況會放置在服務器端的Session中,當用戶在應用內跳轉頁面時,相應的狀態會保留在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'
})
從LoginController
可以看出,constants
可以像service一樣方便注入;
//用戶權限
.constant('USER_ROLES', {
all: '*',
admin: 'admin',
editor: 'editor',
guest: 'guest'
})
用戶登錄狀態和用戶權限將保存在Session中。
登錄服務AuthService
將登錄實現以及用戶權限管理統一交給AuthService
,可在頂層模塊中注冊該服務,這里是app.js
中的myApp
模塊。
.factory('AuthService', function ($http, Session) {
var authService = {};
authService.login = function (credentials) {
//本地提供的服務,可用loopback快速搭建
var api = $resource('http://localhost:3000/api/user_tests');
//因為沒有寫服務端驗證用戶密碼,使用save是為了方便;
//這里,如果服務端已存在該credentials,返回的response會包含錯誤信息,可用來替代401、403等;
return api.save(credentials)
.$promise
.then(function(res) {
Session.create(res.id, res.id,
res.Role);
return res;
});
};
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;
})
Session
用戶登錄后,將服務器中關於用戶的Session存儲起來。
在myApp
模塊中注冊一個服務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;
};
})
用戶信息
當用戶登錄之后,用戶的信息(用戶名、id等)應該保存在哪里?
這里的做法是將用戶對象currentUser
保存在應用頂層模塊myApp
的$scope
中,由於它位於$scope
根部,應用中任何$scope
都繼承它,子代$scope
可以很方便地使用根的變量和方法。
.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
以便在子代$scope
中使用;因為在子代$scope
中直接給currentUser
賦值不會更新根部的currentUser
,而是在當前$scope
中新建一個currentUser
(詳細查詢scope的繼承),所以用setCurrentUser
給根'$scope'的currentUser
變量賦值。
訪問控制
客戶端不存在真正意義的訪問控制,畢竟代碼在客戶端手中,這種工作通常是在服務端完成的,這里說的實際上是顯示控制(visibility control).
AngularJs隱藏信息
ng-show
和ng-hide
是對DOM進行操作,會增加瀏覽器負擔;這里選擇使用ng-if
和ng-switch
。
在view2.html
中插入:
<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
下可以通過傳參進行限制,規定頁面允許訪問的角色:
.config(function ($stateProvider, USER_ROLES) {
$stateProvider.state('dashboard', {
url: '/dashboard',
templateUrl: 'dashboard/index.html',
data: {
authorizedRoles: [USER_ROLES.admin, USER_ROLES.editor]
}
});
})
接下來,需要在每次頁面改變前判斷用戶是否有權限訪問,通過監聽$stateChangeStart
來實現:
.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 expiration)
向服務器發送請求,如果出現非法訪問等情況,服務端將返回HTTP response會包含相應的錯誤信息,例如:
- 401 Unauthorized — 用戶未登錄
- 403 Forbidden — 已登錄,但無權限訪問
- 419 Authentication Timeout (non standard) — 會話過期
- 440 Login Timeout (Microsoft only) — 會話過期
返回401、419、440時,需要彈出登錄框讓用戶登錄;
返回403時,需要彈出錯誤信息;
為了方便,這里的登錄框使用Angular
的directive
封裝,提供一個叫LoginDialog
的標簽。
.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);
}
};
})
loginDialog
的實現如下,通過監聽AUTH_EVENTS.notAuthenticated
和AUTH_EVENTS.sessionTimeout
,當用戶 未登錄/會話過期 時,將loginDialog
的visible
設為true
,顯示登錄框:
.directive('loginDialog', function (AUTH_EVENTS) {
return {
restrict: 'A',
template: '<div ng-if="visible" ng-include="\'view1/view1.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)
}
};
})
為方便測試,將其放入index.html
中:
<body ng-controller='ApplicationController'>
<div login-dialog ng-if="NotLoginPage"></div>
<ul class="menu">
<li><a href="#!/view1">view1</a></li>
<li><a href="#!/view2">view2</a></li>
</ul>
...
到這里,登錄涉及的主要模塊已經完成。