AngularJSauthentication+ RESTful API

用于Auth /(重新)路由的angular度+ RESTful客户端通信(带有API)

这已经在几个不同的问题和几个不同的教程中被涵盖了,但是我所遇到的所有以前的资源并不完全相同。

在一个坚果壳里,我需要

  • 通过POST从http://client.foologin到http://api.foo/login
  • 为提供logout路由的用户提供“login”GUI /组件状态
  • 当用户注销/注销时,能够“更新”用户界面。 这是最令人沮丧的
  • 保护我的路线,以检查身份validation状态(他们是否需要它),并相应地将用户redirect到login页面

我的问题是

  • 每当我导航到一个不同的页面,我需要打电话给api.foo/status以确定用户是否login。(ATM我使用Express的路线)这会导致一个呃,Angular确定的东西ng-show="user.is_authenticated"
  • 当我成功login/注销时,我需要刷新页面(我不想这样做),以填充{{user.first_name}}类的东西,或者在注销的情况下,清空该值出。
 // Sample response from `/status` if successful { customer: {...}, is_authenticated: true, authentication_timeout: 1376959033, ... } 

我试过了

  • http://witoldsz.github.io/angular-http-auth/ 1
  • http://www.frederiknakstad.com/authentication-in-single-page-applications-with-angular-js/ 2
  • https://github.com/mgonto/restangular (对于我的生活,我无法弄清楚如何使用post data POST ,而不是query params 。文档在这个问题上什么也没有。

为什么我觉得我正在失去理智

  • 似乎每个教程都依赖于一些数据库(大量的Mongo,Couch,PHP + MySQL,无限广告)解决scheme,而且没有一个纯粹依靠与RESTful API的通信来保持login状态。 一旦login,更多的POST / GET与withCredentials:true一起发送withCredentials:true ,所以这不是问题
  • 我找不到任何做Angular + REST + Auth的例子/教程/回购,没有后端语言。

我不是很自豪

无可否认,我是Angular的新手,如果我以一种荒谬的方式来解决这个问题,我不会感到惊讶。 如果有人提出一个替代scheme,即使是“坚果汤”,我也会很高兴。

我使用Express主要是因为我非常喜欢JadeStylus – 如果我只想使用Angular的路由,我不会selectExpress的路由,并且会放弃它。

提前感谢任何人都可以提供的帮助。 请不要问我谷歌,因为我有大约26页的紫色链接。 😉


1这个解决scheme依赖于Angular的$ httpBackend模拟,现在还不清楚如何让它与真实的服务器交谈。

2这是最接近的,但由于我有一个现有的API我需要进行身份validation,我不能使用护照的“localStrategy”,并且写一个OAUTH服务似乎是疯狂的 …只有我打算使用。

这是从我的博客文章的URL路由授权和元素安全性在这里,但我会简要地总结要点:-)

前端Web应用程序中的安全性仅仅是阻止Joe Public的一个开始措施,但是具有一些Web知识的任何用户都可以绕过它,所以您应该始终具有安全的服务器端。

围绕angular度安全的主要担忧是路由安全性,幸运的是,当定义一个angular度路由时,您将创build一个对象,一个对象可以具有其他属性。 我的方法的基石是向这个路由对象添加一个安全对象,它基本上定义了用户必须能够访问特定路由的angular色。

  // route which requires the user to be logged in and have the 'Admin' or 'UserManager' permission $routeProvider.when('/admin/users', { controller: 'userListCtrl', templateUrl: 'js/modules/admin/html/users.tmpl.html', access: { requiresLogin: true, requiredPermissions: ['Admin', 'UserManager'], permissionType: 'AtLeastOne' }); 

整个方法主要围绕一个授权服务,基本上检查用户是否具有所需的权限。 这个服务从这个解决scheme的其他部分抽象出关于用户和他们在login期间从服务器检索到的实际许可的问题。 虽然代码非常冗长,但在我的博客文章中有详细的解释。 但是,它基本上是处理权限检查和两种授权方式。 首先是用户必须至less拥有所定义的权限,其次是用户必须拥有所有已定义的权限。

 angular.module(jcs.modules.auth.name).factory(jcs.modules.auth.services.authorization, [ 'authentication', function (authentication) { var authorize = function (loginRequired, requiredPermissions, permissionCheckType) { var result = jcs.modules.auth.enums.authorised.authorised, user = authentication.getCurrentLoginUser(), loweredPermissions = [], hasPermission = true, permission, i; permissionCheckType = permissionCheckType || jcs.modules.auth.enums.permissionCheckType.atLeastOne; if (loginRequired === true && user === undefined) { result = jcs.modules.auth.enums.authorised.loginRequired; } else if ((loginRequired === true && user !== undefined) && (requiredPermissions === undefined || requiredPermissions.length === 0)) { // Login is required but no specific permissions are specified. result = jcs.modules.auth.enums.authorised.authorised; } else if (requiredPermissions) { loweredPermissions = []; angular.forEach(user.permissions, function (permission) { loweredPermissions.push(permission.toLowerCase()); }); for (i = 0; i < requiredPermissions.length; i += 1) { permission = requiredPermissions[i].toLowerCase(); if (permissionCheckType === jcs.modules.auth.enums.permissionCheckType.combinationRequired) { hasPermission = hasPermission && loweredPermissions.indexOf(permission) > -1; // if all the permissions are required and hasPermission is false there is no point carrying on if (hasPermission === false) { break; } } else if (permissionCheckType === jcs.modules.auth.enums.permissionCheckType.atLeastOne) { hasPermission = loweredPermissions.indexOf(permission) > -1; // if we only need one of the permissions and we have it there is no point carrying on if (hasPermission) { break; } } } result = hasPermission ? jcs.modules.auth.enums.authorised.authorised : jcs.modules.auth.enums.authorised.notAuthorised; } return result; }; 

既然路由具有安全性,则需要一种确定用户是否可以在路由更改启动时访问路由的方法。 要做到这一点,我们拦截路由变更请求,检查路由对象(与我们的新访问对象),如果用户不能访问视图,我们用另一个replace路线。

 angular.module(jcs.modules.auth.name).run([ '$rootScope', '$location', jcs.modules.auth.services.authorization, function ($rootScope, $location, authorization) { $rootScope.$on('$routeChangeStart', function (event, next) { var authorised; if (next.access !== undefined) { authorised = authorization.authorize(next.access.loginRequired, next.access.permissions, next.access.permissionCheckType); if (authorised === jcs.modules.auth.enums.authorised.loginRequired) { $location.path(jcs.modules.auth.routes.login); } else if (authorised === jcs.modules.auth.enums.authorised.notAuthorised) { $location.path(jcs.modules.auth.routes.notAuthorised).replace(); } } }); }]); 

这里的关键在于'.replace()',因为它将我们redirect的路由replace为当前路由(他们没有权限看到的路由)。 这停止任何导航回到未经授权的路线。

现在我们可以拦截路线,我们可以做很多很酷的事情,包括login后redirect,如果用户登陆到他们需要login的路线。

解决scheme的第二部分是能够隐藏/显示用户的UI元素取决于那里的权利。 这是通过一个简单的指令来实现的。

 angular.module(jcs.modules.auth.name).directive('access', [ jcs.modules.auth.services.authorization, function (authorization) { return { restrict: 'A', link: function (scope, element, attrs) { var makeVisible = function () { element.removeClass('hidden'); }, makeHidden = function () { element.addClass('hidden'); }, determineVisibility = function (resetFirst) { var result; if (resetFirst) { makeVisible(); } result = authorization.authorize(true, roles, attrs.accessPermissionType); if (result === jcs.modules.auth.enums.authorised.authorised) { makeVisible(); } else { makeHidden(); } }, roles = attrs.access.split(','); if (roles.length > 0) { determineVisibility(true); } } }; }]); 

然后你会确定一个像这样的元素:

  <button type="button" access="CanEditUser, Admin" access-permission-type="AtLeastOne">Save User</button> 

阅读我的完整博客文章,以获得更详细的方法概述。

我已经为UserApp编写了一个AngularJS模块 ,可以完成你所要求的任何事情。 你可以:

  1. 修改模块并将函数附加到您自己的API或
  2. 将该模块与用户pipe理API UserApp一起使用

https://github.com/userapp-io/userapp-angular

它支持保护/公共路由,login/注销时重新路由,心跳状态检查,将会话令牌存储在cookie中,事件等。

如果您想尝试使用UserApp,请参加Codecademy课程 。

下面是一些如何工作的例子:

  • 具有error handling的login表单:

     <form ua-login ua-error="error-msg"> <input name="login" placeholder="Username"><br> <input name="password" placeholder="Password" type="password"><br> <button type="submit">Log in</button> <p id="error-msg"></p> </form> 
  • registry单与error handling:

     <form ua-signup ua-error="error-msg"> <input name="first_name" placeholder="Your name"><br> <input name="login" ua-is-email placeholder="Email"><br> <input name="password" placeholder="Password" type="password"><br> <button type="submit">Create account</button> <p id="error-msg"></p> </form> 
  • 如何指定哪些路由应该公开,哪些路由是login表单:

     $routeProvider.when('/login', {templateUrl: 'partials/login.html', public: true, login: true}); $routeProvider.when('/signup', {templateUrl: 'partials/signup.html', public: true}); 

    .otherwise()路由应该设置为您希望用户在login后redirect的位置。 例:

    $routeProvider.otherwise({redirectTo: '/home'});

  • 注销链接:

    <a href="#" ua-logout>Log Out</a>

    (结束会话并redirect到login路由)

  • 访问用户属性:

    用户信息使用user服务进行访问,例如: user.current.email

    或者在模板中: <span>{{ user.email }}</span>

  • 隐藏仅在login时才可见的元素:

    <div ng-show="user.authorized">Welcome {{ user.first_name }}!</div>

  • 根据权限显示一个元素:

    <div ua-has-permission="admin">You are an admin</div>

为了validation您的后端服务,只需使用user.token()获取会话令牌并通过AJAX请求发送。 在后端,使用UserApp API (如果您使用UserApp)检查令牌是否有效。

如果您需要任何帮助,请让我知道:)

我没有使用$资源,因为我只是手工制作我的应用程序的服务电话。 这就是说,我已经通过一个服务来处理login,这个服务依赖于所有其他获得某种初始化数据的服务。 当login成功时,触发所有服务的初始化。

在我的控制器范围内,我观察loginServiceInformation并相应地填充模型的某些属性(以触发适当的ng-show / hide)。 关于路由我使用Angular的内置路由,我只是有一个基于这里显示的loggedIn布尔的ng-hide,它显示的文本请求login或其他与ng-view属性的div(所以如果没有loginlogin后立即在正确的页面上,目前我加载数据的所有意见,但我相信这可能是更多的select性,如果有必要)

 //Services angular.module("loginModule.services", ["gardenModule.services", "surveyModule.services", "userModule.services", "cropModule.services" ]).service( 'loginService', [ "$http", "$q", "gardenService", "surveyService", "userService", "cropService", function ( $http, $q, gardenService, surveyService, userService, cropService) { var service = { loginInformation: {loggedIn:false, username: undefined, loginAttemptFailed:false, loggedInUser: {}, loadingData:false}, getLoggedInUser:function(username, password) { service.loginInformation.loadingData = true; var deferred = $q.defer(); $http.get("php/login/getLoggedInUser.php").success(function(data){ service.loginInformation.loggedIn = true; service.loginInformation.loginAttemptFailed = false; service.loginInformation.loggedInUser = data; gardenService.initialize(); surveyService.initialize(); userService.initialize(); cropService.initialize(); service.loginInformation.loadingData = false; deferred.resolve(data); }).error(function(error) { service.loginInformation.loggedIn = false; deferred.reject(error); }); return deferred.promise; }, login:function(username, password) { var deferred = $q.defer(); $http.post("php/login/login.php", {username:username, password:password}).success(function(data){ service.loginInformation.loggedInUser = data; service.loginInformation.loggedIn = true; service.loginInformation.loginAttemptFailed = false; gardenService.initialize(); surveyService.initialize(); userService.initialize(); cropService.initialize(); deferred.resolve(data); }).error(function(error) { service.loginInformation.loggedInUser = {}; service.loginInformation.loggedIn = false; service.loginInformation.loginAttemptFailed = true; deferred.reject(error); }); return deferred.promise; }, logout:function() { var deferred = $q.defer(); $http.post("php/login/logout.php").then(function(data){ service.loginInformation.loggedInUser = {}; service.loginInformation.loggedIn = false; deferred.resolve(data); }, function(error) { service.loginInformation.loggedInUser = {}; service.loginInformation.loggedIn = false; deferred.reject(error); }); return deferred.promise; } }; service.getLoggedInUser(); return service; }]); //Controllers angular.module("loginModule.controllers", ['loginModule.services']).controller("LoginCtrl", ["$scope", "$location", "loginService", function($scope, $location, loginService){ $scope.loginModel = { loadingData:true, inputUsername: undefined, inputPassword: undefined, curLoginUrl:"partials/login/default.html", loginFailed:false, loginServiceInformation:{} }; $scope.login = function(username, password) { loginService.login(username,password).then(function(data){ $scope.loginModel.curLoginUrl = "partials/login/logoutButton.html"; }); } $scope.logout = function(username, password) { loginService.logout().then(function(data){ $scope.loginModel.curLoginUrl = "partials/login/default.html"; $scope.loginModel.inputPassword = undefined; $scope.loginModel.inputUsername = undefined; $location.path("home"); }); } $scope.switchUser = function(username, password) { loginService.logout().then(function(data){ $scope.loginModel.curLoginUrl = "partials/login/loginForm.html"; $scope.loginModel.inputPassword = undefined; $scope.loginModel.inputUsername = undefined; }); } $scope.showLoginForm = function() { $scope.loginModel.curLoginUrl = "partials/login/loginForm.html"; } $scope.hideLoginForm = function() { $scope.loginModel.curLoginUrl = "partials/login/default.html"; } $scope.$watch(function(){return loginService.loginInformation}, function(newVal) { $scope.loginModel.loginServiceInformation = newVal; if(newVal.loggedIn) { $scope.loginModel.curLoginUrl = "partials/login/logoutButton.html"; } }, true); }]); angular.module("loginModule", ["loginModule.services", "loginModule.controllers"]); 

HTML

 <div style="height:40px;z-index:200;position:relative"> <div class="well"> <form ng-submit="login(loginModel.inputUsername, loginModel.inputPassword)"> <input type="text" ng-model="loginModel.inputUsername" placeholder="Username"/><br/> <input type="password" ng-model="loginModel.inputPassword" placeholder="Password"/><br/> <button class="btn btn-primary">Submit</button> <button class="btn" ng-click="hideLoginForm()">Cancel</button> </form> <div ng-show="loginModel.loginServiceInformation.loginAttemptFailed"> Login attempt failed </div> </div> </div> 

使用以上部分完成图片的Base HTML:

 <body ng-controller="NavigationCtrl" ng-init="initialize()"> <div id="outerContainer" ng-controller="LoginCtrl"> <div style="height:20px"></div> <ng-include src="'partials/header.html'"></ng-include> <div id="contentRegion"> <div ng-hide="loginModel.loginServiceInformation.loggedIn">Please login to continue. <br/><br/> This new version of this site is currently under construction. <br/><br/> If you need the legacy site and database <a href="legacy/">click here.</a></div> <div ng-view ng-show="loginModel.loginServiceInformation.loggedIn"></div> </div> <div class="clear"></div> <ng-include src="'partials/footer.html'"></ng-include> </div> </body> 

我有login控制器在DOM中更高的ng控制器定义,以便我可以根据loggedInvariables更改我的页面的主体区域。

注意我还没有在这里实现表单validation。 无可否认,对于Angular来说还是相当新鲜的,所以对这篇文章中的东西的任何指针都是受欢迎的。 虽然这不直接回答这个问题,因为它不是一个基于REST的实现,我相信同样可以适应$资源,因为它build立在$ http调用之上。

我创build了一个github回购总结这篇文章基本上: https : //medium.com/opinionated-angularjs/techniques-for-authentication-in-angularjs-applications-7bbf0346acec

ng-login Github回购

Plunker

我会尽力解释,希望我能帮你们中的一些人:

(1)app.js:在应用程序定义上创buildauthentication常量

 var loginApp = angular.module('loginApp', ['ui.router', 'ui.bootstrap']) /*Constants regarding user login defined here*/ .constant('USER_ROLES', { all : '*', admin : 'admin', editor : 'editor', guest : 'guest' }).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' }) 

(2)authentication服务:以下所有function都在auth.js服务中实现。 $ http服务用于与服务器进行通信以进行authentication过程。 还包含授权function,即允许用户执行特定操作。

 angular.module('loginApp') .factory('Auth', [ '$http', '$rootScope', '$window', 'Session', 'AUTH_EVENTS', function($http, $rootScope, $window, Session, AUTH_EVENTS) { authService.login() = [...] authService.isAuthenticated() = [...] authService.isAuthorized() = [...] authService.logout() = [...] return authService; } ]); 

(3)会话:保持用户数据的单例。 这里的实现取决于你。

 angular.module('loginApp').service('Session', function($rootScope, USER_ROLES) { this.create = function(user) { this.user = user; this.userRole = user.userRole; }; this.destroy = function() { this.user = null; this.userRole = null; }; return this; }); 

(4)家长控制器:将其视为应用程序的“主要”function,所有控制器都从此控制器inheritance,并且是此应用程序的身份validation的主干。

 <body ng-controller="ParentController"> [...] </body> 

(5)访问控制:拒绝某些路线的访问必须执行两个步骤:

a)在ui路由器的$ stateProvider服务中添加允许访问每个路由的angular色的数据,如下所示(同样可以用于ngRoute)。

 .config(function ($stateProvider, USER_ROLES) { $stateProvider.state('dashboard', { url: '/dashboard', templateUrl: 'dashboard/index.html', data: { authorizedRoles: [USER_ROLES.admin, USER_ROLES.editor] } }); }) 

b)在$ rootScope。$ on('$ stateChangeStart')上添加防止状态改变的function,如果用户没有被授权的话。

 $rootScope.$on('$stateChangeStart', function (event, next) { var authorizedRoles = next.data.authorizedRoles; if (!Auth.isAuthorized(authorizedRoles)) { event.preventDefault(); if (Auth.isAuthenticated()) { // user is not allowed $rootScope.$broadcast(AUTH_EVENTS.notAuthorized); } else {d // user is not logged in $rootScope.$broadcast(AUTH_EVENTS.notAuthenticated); } } }); 

(6)Auth拦截器:这是实现的,但不能在这个代码的范围内检查。 在每个$ http请求之后,这个拦截器检查状态码,如果下面的一个返回,那么它会广播一个事件来强制用户重新login。

 angular.module('loginApp') .factory('AuthInterceptor', [ '$rootScope', '$q', 'Session', 'AUTH_EVENTS', function($rootScope, $q, Session, 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); } }; } ]); 

PS通过添加directives.js中包含的指令,可以轻松避免第1条中所述的表单数据自动填充错误。

PS2这个代码可以很容易地由用户调整,以允许不同的路线被看到,或者显示不被显示的内容。 逻辑必须在服务器端实现,这只是在ng-app上正确显示的东西。

Interesting Posts