多数AngularJs应用离不开登陆操做,最近阅读了一篇关于AngularJs登陆的博客,博客中实现的登陆系统demo可以应用于多数小型AngularJs应用,实现也并不困难,这里讲讲如何实现这个简单的登陆系统。css
这里使用的种子项目是 angular-seed
,登陆系统会在这个种子项目的基础上完成
,github地址:https://github.com/angular/angular-seed/。按照github上`README.md`配置后即可在上面添加咱们本身的登陆系统。html
angular-seed文件目录:ios
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将做为登陆界面。git
一个简单实用的登陆表单的html文件:angularjs
<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
.以下:github
// 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
里面建议多使用抽象逻辑,而非具体的实现。c#
一般,用户的登陆状况会放置在服务器端的Session中,当用户在应用内跳转页面时,相应的状态会保留在Session中。这里先定义__用户登陆的状态__和__用户权限__,这里使用constants定义:api
//用户登陆状态 .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同样方便注入;promise
//用户权限 .constant('USER_ROLES', { all: '*', admin: 'admin', editor: 'editor', guest: 'guest' })
用户登陆状态和用户权限将保存在Session中。浏览器
将登陆实现以及用户权限管理统一交给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会包含错误信息,可用来替代40一、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存储起来。
在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).
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); } } }); })
若是用户 未登陆/无权限,将被限制在当前页面,发出 认证失败/受权失败 的广播;
以后,须要有相应的交互,如弹出登陆框,提醒用户完成登陆操做,或者弹出错误提示,告诉用户无权限访问相应的页面。
向服务器发送请求,若是出现非法访问等状况,服务端将返回HTTP response会包含相应的错误信息,例如:
返回40一、41九、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> ...
到这里,登陆涉及的主要模块已经完成。