AngularJS身份验证+ RESTful API

cou*_*zzi 71 javascript restful-authentication node.js angularjs angular-http-interceptors

用于Auth /(重新)路由的Angular + RESTful客户端通信w/API

这已经在几个不同的问题中得到了解决,并且在一些不同的教程中,但我遇到的所有以前的资源并没有完全触及.

在坚果壳中,我需要

  • 通过POST登录http://client.foohttp://api.foo/login
  • 为提供logout路由的用户提供"登录"GUI /组件状态
  • 能够在用户注销/注销时"更新"UI. 这是最令人沮丧的
  • 保护我的路由以检查身份验证状态(如果需要)并相应地将用户重定向到登录页面

我的问题是

  • 每次我导航到不同的页面时,我都需要调用以api.foo/status确定用户是否已登录.(ATM我正在使用Express进行路由)这会导致打嗝,因为Angular会确定ng-show="user.is_authenticated"
  • 当我成功登录/注销时,我需要刷新页面(我不想这样做)以填充类似的东西{{user.first_name}},或者在注销的情况下,清空该值.
// Sample response from `/status` if successful 

{
   customer: {...},
   is_authenticated: true,
   authentication_timeout: 1376959033,
   ...
}
Run Code Online (Sandbox Code Playgroud)

我试过的

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

  • 似乎每个教程都依赖于一些数据库(许多Mongo,Couch,PHP + MySQL,无限广告)解决方案,并且没有一个完全依赖于与RESTful API的通信来持久登录状态.登录后,会发送其他POST/GET withCredentials:true,因此不是问题
  • 我找不到任何做Angular + REST + Auth的示例/教程/回购,没有后端语言.

我并不太自豪

不可否认,我是Angular的新手,如果我以荒谬的方式接近它,也不会感到惊讶; 如果有人建议替代方案,我会很激动 - 即使它是汤到坚果.

我的使用Express主要是因为我真的很喜欢- 我JadeStylus没有嫁给Express'路由,如果我想做的事情只能用于Angular的路由,我会放弃它.

提前感谢任何人都可以提供的帮助.请不要问我Google,因为我有大约26页的紫色链接.;-)


1这个解决方案依赖于Angular的$ httpBackend模拟,并且不清楚如何使它与真实服务器通信.

2这是最接近的,但由于我有一个现有的API我需要进行身份验证,我不能使用护照的'localStrategy',写一个OAUTH服务似乎很疯狂 ...只有我打算使用.

Jon*_*Jon 34

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

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

关于角度安全性的主要问题是路径安全性,幸运的是,在角度定义路径时,您要创建一个对象,一个可以具有其他属性的对象.我的方法的基础是向此路由对象添加一个安全对象,该对象基本上定义了用户必须具有的角色才能访问特定路由.

 // 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'
        });
Run Code Online (Sandbox Code Playgroud)

整个方法围绕授权服务,该服务基本上进行检查以查看用户是否具有所需的权限.此服务将此问题的其他部分的问题从用户及其在登录期间从服务器检索到的实际权限中抽象出来.虽然代码非常详细,但在我的博客文章中有详细解释.但是,它基本上处理权限检查和两种授权模式.第一个是用户必须至少具有已定义的权限,第二个是用户必须具有所有已定义的权限.

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;
};
Run Code Online (Sandbox Code Playgroud)

现在路由具有安全性,您需要一种方法来确定用户是否可以在路由更改开始时访问路由.为此,我们拦截路由更改请求,检查路由对象(使用我们的新访问对象),如果用户无法访问视图,我们将路由替换为另一个.

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();
                }
            }
        });
    }]);
Run Code Online (Sandbox Code Playgroud)

这里的关键实际上是'.replace()',因为它将我们重定向到的路由替换为当前路由(他们没有权限看到的路由).然后停止任何导航回未经授权的路线.

现在我们可以拦截路线,我们可以做很多很酷的事情,包括登录后重定向,如果用户登陆他们需要登录的路线.

解决方案的第二部分是能够根据权限隐藏/向用户显示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);
                  }
              }
            };
        }]);
Run Code Online (Sandbox Code Playgroud)

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

 <button type="button" access="CanEditUser, Admin" access-permission-type="AtLeastOne">Save User</button>
Run Code Online (Sandbox Code Playgroud)

阅读我的完整博客文章,了解该方法的更详细概述.


sha*_*ain 5

我没有使用 $resource 因为我只是为我的应用程序手工制作服务调用。也就是说,我通过拥有一个服务来处理登录,该服务依赖于获取某种初始化数据的所有其他服务。当登录成功时,它会触发所有服务的初始化。

在我的控制器范围内,我观察 loginServiceInformation 并相应地填充模型的一些属性(以触发适当的 ng-show/hide)。关于路由,我使用的是 Angular 的内置路由,我只是有一个基于这里显示的 loginIn 布尔值的 ng-hide,它显示请求登录的文本,否则显示带有 ng-view 属性的 div(所以如果没有登录登录后立即进入正确的页面,目前我加载所有视图的数据,但我相信如果有必要,这可能更具选择性)

//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"]);
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

使用上述部分完成图片的基础 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>
Run Code Online (Sandbox Code Playgroud)

我在 DOM 中用 ng-controller 定义了登录控制器,以便我可以根据 loginIn 变量更改页面的正文区域。

注意我还没有在这里实现表单验证。诚然,Angular 仍然很新鲜,因此欢迎对本文中的内容提出任何建议。虽然这不能直接回答问题,因为它不是基于 RESTful 的实现,但我相信同样可以适用于 $resources,因为它建立在 $http 调用之上。


Tim*_*son 5

我为UserApp编写了一个AngularJS模块,它可以满足您的所有要求.你可以:

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

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

它支持受保护/公共路由,在登录/注销时重新路由,用于状态检查的心跳,将会话令牌存储在cookie中,事件等.

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

以下是一些如何工作的示例:

  • 登录表单,错误处理:

    <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>
    
    Run Code Online (Sandbox Code Playgroud)
  • 具有错误处理的注册表单:

    <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>
    
    Run Code Online (Sandbox Code Playgroud)
  • 如何指定应该公开的路由以及登录表单的路由:

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

    .otherwise()路径应设置为您希望您的用户登录后重定向.例:

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

  • 退出链接:

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

    (结束会话并重定向到登录路由)

  • 访问用户属性:

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

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

  • 隐藏仅在登录时可见的元素:

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

  • 根据权限显示元素:

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

要对您的后端服务进行身份验证,只需使用user.token()获取会话令牌并使用AJAX请求发送它.在后端,使用UserApp API(如果使用UserApp)检查令牌是否有效.

如果您需要任何帮助,请告诉我:)

  • 这是付费解决方案,不是吗? (15认同)