AngularJSauthentication+ RESTful API
用于Auth /(重新)路由的angular度+ RESTful客户端通信(带有API)
这已经在几个不同的问题和几个不同的教程中被涵盖了,但是我所遇到的所有以前的资源并不完全相同。
在一个坚果壳里,我需要
- 通过POST从
http://client.foo
login到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
主要是因为我非常喜欢Jade
和Stylus
– 如果我只想使用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模块 ,可以完成你所要求的任何事情。 你可以:
- 修改模块并将函数附加到您自己的API或
- 将该模块与用户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上正确显示的东西。