AngularJS ui-router login authentication
我刚接触过AngularJS,我对如何在以下场景中使用Angular-"UI路由器"有点困惑:
我正在构建一个由两部分组成的Web应用程序。第一部分是主页及其登录和注册视图,第二部分是仪表板(成功登录后)。
我为主页部分创建了一个
现在我完成了仪表板部分,不知道如何将这两个部分与它们不同角度的应用程序结合起来。我如何才能告诉家庭应用程序重定向到仪表板应用程序?
我正在制作一个更好的演示,并将其中的一些服务清理到一个可用的模块中,但下面是我想到的。这是一个复杂的过程,需要解决一些注意事项,所以请坚持下去。你得把它分成几块。好的。
看看这个垃圾。好的。
首先,您需要一个服务来存储用户的身份。我把这个叫做
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | .factory('principal', ['$q', '$http', '$timeout', function($q, $http, $timeout) { var _identity = undefined, _authenticated = false; return { isIdentityResolved: function() { return angular.isDefined(_identity); }, isAuthenticated: function() { return _authenticated; }, isInRole: function(role) { if (!_authenticated || !_identity.roles) return false; return _identity.roles.indexOf(role) != -1; }, isInAnyRole: function(roles) { if (!_authenticated || !_identity.roles) return false; for (var i = 0; i < roles.length; i++) { if (this.isInRole(roles[i])) return true; } return false; }, authenticate: function(identity) { _identity = identity; _authenticated = identity != null; }, identity: function(force) { var deferred = $q.defer(); if (force === true) _identity = undefined; // check and see if we have retrieved the // identity data from the server. if we have, // reuse it by immediately resolving if (angular.isDefined(_identity)) { deferred.resolve(_identity); return deferred.promise; } // otherwise, retrieve the identity data from the // server, update the identity object, and then // resolve. // $http.get('/svc/account/identity', // { ignoreErrors: true }) // .success(function(data) { // _identity = data; // _authenticated = true; // deferred.resolve(_identity); // }) // .error(function () { // _identity = null; // _authenticated = false; // deferred.resolve(_identity); // }); // for the sake of the demo, fake the lookup // by using a timeout to create a valid // fake identity. in reality, you'll want // something more like the $http request // commented out above. in this example, we fake // looking up to find the user is // not logged in var self = this; $timeout(function() { self.authenticate(null); deferred.resolve(_identity); }, 1000); return deferred.promise; } }; } ]) |
第二,您需要一个服务来检查用户想要进入的状态,确保他们已经登录(如果需要;不需要登录、密码重置等),然后进行角色检查(如果您的应用程序需要这样做的话)。如果它们没有经过身份验证,请将它们发送到登录页。如果它们经过身份验证,但角色检查失败,请将它们发送到拒绝访问页面。我把这项服务叫做
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | .factory('authorization', ['$rootScope', '$state', 'principal', function($rootScope, $state, principal) { return { authorize: function() { return principal.identity() .then(function() { var isAuthenticated = principal.isAuthenticated(); if ($rootScope.toState.data.roles && $rootScope.toState .data.roles.length > 0 && !principal.isInAnyRole( $rootScope.toState.data.roles)) { if (isAuthenticated) { // user is signed in but not // authorized for desired state $state.go('accessdenied'); } else { // user is not authenticated. Stow // the state they wanted before you // send them to the sign-in state, so // you can return them when you're done $rootScope.returnToState = $rootScope.toState; $rootScope.returnToStateParams = $rootScope.toStateParams; // now, send them to the signin state // so they can log in $state.go('signin'); } } }); } }; } ]) |
现在你所要做的就是听听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | .run(['$rootScope', '$state', '$stateParams', 'authorization', 'principal', function($rootScope, $state, $stateParams, authorization, principal) { $rootScope.$on('$stateChangeStart', function(event, toState, toStateParams) { // track the state the user wants to go to; // authorization service needs this $rootScope.toState = toState; $rootScope.toStateParams = toStateParams; // if the principal is resolved, do an // authorization check immediately. otherwise, // it'll be done when the state it resolved. if (principal.isIdentityResolved()) authorization.authorize(); }); } ]); |
跟踪用户身份的棘手部分是,如果您已经过身份验证(例如,您在上一个会话之后访问了该页,并在cookie中保存了身份验证令牌,或者您硬刷新了一个页面,或者从链接中放到了一个URL上),则会查找它。由于
1 2 3 4 5 6 7 8 9 10 11 | $stateProvider.state('site', { 'abstract': true, resolve: { authorize: ['authorization', function(authorization) { return authorization.authorize(); } ] }, template: '' }) |
这里还有一个问题…
好吧,那么到目前为止我们做了什么?好的。
我们从这里去哪里?嗯,您可以将州组织成需要登录的区域。您可以通过将
1 2 3 4 5 6 7 8 9 10 11 12 | .state('restricted', { parent: 'site', url: '/restricted', data: { roles: ['Admin'] }, views: { 'content@': { templateUrl: 'restricted.html' } } }) |
现在,您可以按状态控制用户可以访问路由的内容。还有其他问题吗?可能只根据是否登录而改变视图的一部分?没问题。使用
首先,将EDOCX1[4]注入控制器或其他对象,并将其固定在范围内,以便在您的视图中轻松使用:好的。
1 2 3 4 5 | .scope('HomeCtrl', ['$scope', 'principal', function($scope, principal) { $scope.principal = principal; }); |
显示或隐藏元素:好的。
1 2 3 4 | I'm logged in I'm not logged in |
等等,等等,等等。无论如何,在您的示例应用程序中,主页的状态将允许未经身份验证的用户访问。他们可以链接到登录或注册状态,或者将这些表单内置到该页面中。任何适合你的。好的。
仪表板页面可能都继承自一个状态,该状态要求用户登录,例如,是
我认为,目前公布的解决方案不必要复杂。有一个简单的方法。
然而,这里有两个简单的选择。挑选一个:
解决方案1:收听你可以听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | angular.module('App', ['ui.router']) // In the run phase of your Angular application .run(function($rootScope, user, $state) { // Listen to '$locationChangeSuccess', not '$stateChangeStart' $rootScope.$on('$locationChangeSuccess', function() { user .logIn() .catch(function() { // log-in promise failed. Redirect to log-in page. $state.go('logInPage') }) }) }) |
请记住,这实际上并不能阻止加载目标状态,但如果用户未经授权,它确实会重定向到登录页。这没关系,因为服务器上有真正的保护。
解决方案2:使用状态在这个解决方案中,您使用
如果用户没有通过身份验证,那么您基本上拒绝
具体情况如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | angular.module('App', ['ui.router']) .config( function($stateProvider) { $stateProvider .state('logInPage', { url: '/logInPage', templateUrl: 'sections/logInPage.html', controller: 'logInPageCtrl', }) .state('myProtectedContent', { url: '/myProtectedContent', templateUrl: 'sections/myProtectedContent.html', controller: 'myProtectedContentCtrl', resolve: { authenticate: authenticate } }) .state('alsoProtectedContent', { url: '/alsoProtectedContent', templateUrl: 'sections/alsoProtectedContent.html', controller: 'alsoProtectedContentCtrl', resolve: { authenticate: authenticate } }) function authenticate($q, user, $state, $timeout) { if (user.isAuthenticated()) { // Resolve the promise successfully return $q.when() } else { // The next bit of code is asynchronously tricky. $timeout(function() { // This code runs after the authentication promise has been rejected. // Go to the log-in page $state.go('logInPage') }) // Reject the authentication promise to prevent the state from loading return $q.reject() } } } ) |
与第一个解决方案不同,此解决方案实际上阻止加载目标状态。
最简单的解决方案是使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | angular .module('myApp', [ 'ui.router', ]) .run(['$rootScope', 'User', '$state', function ($rootScope, User, $state) { $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) { if (toState.name !== 'auth' && !User.authenticaded()) { event.preventDefault(); $state.go('auth'); } }); }] ); |
我认为您需要一个处理身份验证过程(及其存储)的
在这项服务中,您需要一些基本方法:
isAuthenticated() login() logout() - 等。。。
此服务应注入每个模块的控制器中:
- 在Dashboard部分,使用此服务检查用户是否经过身份验证(
service.isAuthenticated() 方法)。如果没有,重定向到/登录 - 在您的登录部分,只需使用表单数据通过您的
service.login() 方法对用户进行身份验证。
这个行为的一个好的、健壮的例子是ProjectAngular应用程序,特别是它的安全模块,它基于可怕的HTTP认证拦截器模块。
希望这有帮助
我创建这个模块是为了帮助制作这个过程。
你可以这样做:
1 2 3 4 5 6 7 8 | $routeProvider .state('secret', { ... permissions: { only: ['admin', 'god'] } }); |
或者也
1 2 3 4 5 6 7 8 | $routeProvider .state('userpanel', { ... permissions: { except: ['not-logged-in'] } }); |
这是全新的,但值得一看!
https://github.com/narzerus/angular-permission网站
我想与用户界面路由器1.0.0.x共享另一个解决方案
如您所知,StateChangeStart和StateChangeSsuccess现在已被弃用。https://github.com/angular-ui/ui-router/issues/2655
相反,您应该使用$transitions http://angular-ui.github.io/ui-router/1.0.0-alpha.1/interfaces/transition.ihookregistry.html
我就是这样做到的:
首先,我有和AuthService具有一些有用的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | angular.module('myApp') .factory('AuthService', ['$http', '$cookies', '$rootScope', function ($http, $cookies, $rootScope) { var service = {}; // Authenticates throug a rest service service.authenticate = function (username, password, callback) { $http.post('api/login', {username: username, password: password}) .success(function (response) { callback(response); }); }; // Creates a cookie and set the Authorization header service.setCredentials = function (response) { $rootScope.globals = response.token; $http.defaults.headers.common['Authorization'] = 'Bearer ' + response.token; $cookies.put('globals', $rootScope.globals); }; // Checks if it's authenticated service.isAuthenticated = function() { return !($cookies.get('globals') === undefined); }; // Clear credentials when logout service.clearCredentials = function () { $rootScope.globals = undefined; $cookies.remove('globals'); $http.defaults.headers.common.Authorization = 'Bearer '; }; return service; }]); |
然后我有了这个配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | angular.module('myApp', [ 'ui.router', 'ngCookies' ]) .config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) { $urlRouterProvider.otherwise('/resumen'); $stateProvider .state("dashboard", { url:"/dashboard", templateUrl:"partials/dashboard.html", controller:"dashCtrl", data: { authRequired: true } }) .state("login", { url:"/login", templateUrl:"partials/login.html", controller:"loginController" }) }]) .run(['$rootScope', '$transitions', '$state', '$cookies', '$http', 'AuthService', function ($rootScope, $transitions, $state, $cookies, $http, AuthService) { // keep user logged in after page refresh $rootScope.globals = $cookies.get('globals') || {}; $http.defaults.headers.common['Authorization'] = 'Bearer ' + $rootScope.globals; $transitions.onStart({ to: function (state) { return state.data != null && state.data.authRequired === true; } }, function () { if (!AuthService.isAuthenticated()) { return $state.target("login"); } }); }]); |
你可以看到我用的
1 2 3 | data: { authRequired: true } |
将状态标记为只有经过身份验证才能访问。
然后,在.run上,我使用转换检查授权状态
1 2 3 4 5 6 7 8 9 | $transitions.onStart({ to: function (state) { return state.data != null && state.data.authRequired === true; } }, function () { if (!AuthService.isAuthenticated()) { return $state.target("login"); } }); |
我使用$transitions文档中的一些代码构建了这个示例。我对用户界面路由器还很熟悉,但它能正常工作。
希望它能帮助任何人。
下面是我们如何摆脱无限路由循环,仍然使用
1 2 3 | if('401' !== toState.name) { if (principal.isIdentityResolved()) authorization.authorize(); } |
使用$HTTP拦截器
通过使用$HTTP拦截器,您可以将头发送到后端或其他方式,并以这种方式进行检查。
关于$HTTP拦截器的伟大文章
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | $httpProvider.interceptors.push(function ($q) { return { 'response': function (response) { // TODO Create check for user authentication. With every request send"headers" or do some other check return response; }, 'responseError': function (reject) { // Forbidden if(reject.status == 403) { console.log('This page is forbidden.'); window.location = '/'; // Unauthorized } else if(reject.status == 401) { console.log("You're not authorized to view this page."); window.location = '/'; } return $q.reject(reject); } }; }); |
把它放到.config或.run函数中。
首先,您需要一个可以注入到控制器中的服务,该服务对应用程序身份验证状态有一定的了解。用本地存储持久化身份验证详细信息是一种很好的方法。
接下来,您需要在状态更改之前检查auth的状态。由于您的应用程序有一些需要验证的页面和其他不需要验证的页面,请创建一个检查验证的父路由,并使所有其他需要验证的页面都成为该父路由的子路由。
最后,您需要某种方法来判断当前登录的用户是否可以执行某些操作。这可以通过向认证服务添加"can"功能来实现。可以接受两个参数:-操作-必需(即"管理仪表板"或"创建新仪表板")。-对象-可选-正在操作的对象。例如,如果您有一个Dashboard对象,您可能希望检查Dashboard.OwnerID===loggedinuser.id。(当然,从客户机传递的信息永远不应该被信任,在将其写入数据库之前,您应该在服务器上对此进行验证)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | angular.module('myApp', ['ngStorage']).config([ '$stateProvider', function( $stateProvider ) { $stateProvider .state('home', {...}) //not authed .state('sign-up', {...}) //not authed .state('login', {...}) //not authed .state('authed', {...}) //authed, make all authed states children .state('authed.dashboard', {...}) }]) .service('context', [ '$localStorage', function( $localStorage ) { var _user = $localStorage.get('user'); return { getUser: function() { return _user; }, authed: function() { return (_user !== null); }, // server should return some kind of token so the app // can continue to load authenticated content without having to // re-authenticate each time login: function() { return $http.post('/login.json').then(function(reply) { if (reply.authenticated === true) { $localStorage.set(_userKey, reply.user); } }); }, // this request should expire that token, rendering it useless // for requests outside of this session logout: function() { return $http.post('logout.json').then(function(reply) { if (reply.authenticated === true) { $localStorage.set(_userKey, reply.user); } }); }, can: function(action, object) { if (!this.authed()) { return false; } var user = this.getUser(); if (user && user.type === 'admin') { return true; } switch(action) { case 'manage_dashboards': return (user.type === 'manager'); } return false; } } }]) .controller('AuthCtrl', [ 'context', '$scope', function( context, $scope ) { $scope.$root.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) { //only require auth if we're moving to another authed page if (toState && toState.name.indexOf('authed') > -1) { requireAuth(); } }); function requireAuth() { if (!context.authed()) { $state.go('login'); } } }] |
**免责声明:以上代码为伪代码,不作任何保证**
我还有另一个解决方案:当您只有您想在登录时显示的内容时,该解决方案非常有效。定义一个规则,在该规则中检查您是否已登录,而不是白名单路由的路径。
1 2 3 4 5 6 7 8 | $urlRouterProvider.rule(function ($injector, $location) { var UserService = $injector.get('UserService'); var path = $location.path(), normalized = path.toLowerCase(); if (!UserService.isLoggedIn() && path.indexOf('login') === -1) { $location.path('/login/signin'); } }); |
在我的示例中,我询问我是否没有登录,并且我要路由的当前路由不是`/login'的一部分,因为我的白名单路由如下
1 2 | /login/signup // registering new user /login/signin // login to app |
所以我可以即时访问这两条路线,如果你在线的话,每一条路线都会被检查。
这是我登录模块的整个路由文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | export default ( $stateProvider, $locationProvider, $urlRouterProvider ) => { $stateProvider.state('login', { parent: 'app', url: '/login', abstract: true, template: '<ui-view></ui-view>' }) $stateProvider.state('signin', { parent: 'login', url: '/signin', template: '<login-signin-directive></login-signin-directive>' }); $stateProvider.state('lock', { parent: 'login', url: '/lock', template: '<login-lock-directive></login-lock-directive>' }); $stateProvider.state('signup', { parent: 'login', url: '/signup', template: '<login-signup-directive></login-signup-directive>' }); $urlRouterProvider.rule(function ($injector, $location) { var UserService = $injector.get('UserService'); var path = $location.path(); if (!UserService.isLoggedIn() && path.indexOf('login') === -1) { $location.path('/login/signin'); } }); $urlRouterProvider.otherwise('/error/not-found'); } |