【问题标题】:AngularJS Authentication + RESTful APIAngularJS 身份验证 + RESTful API
【发布时间】:2013-08-21 22:19:20
【问题描述】:

Angular+RESTful 客户端通信 w/API 用于 Auth/(re)Routing

这已经在几个不同的问题和几个不同的教程中讨论过,但是我之前遇到的所有资源都没有完全准确。

简而言之,我需要

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

我的问题是

  • 每次我导航到不同的页面时,我都需要调用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,
   ...
}

我的尝试

为什么我觉得我疯了

  • 似乎每个教程都依赖于一些数据库(大量的 Mongo、Couch、PHP+MySQL 等)解决方案,没有一个教程完全依赖与 RESTful API 的通信来保持登录状态。登录后,会使用 withCredentials:true 发送额外的 POST/GET,所以这不是问题
  • 我找不到任何执行 Angular+REST+Auth、无后端语言的示例/教程/存储库。

我不太骄傲

诚然,我是 Angular 的新手,如果我以一种荒谬的方式处理这个问题,我不会感到惊讶;如果有人提出替代方案,我会很高兴——即使是从汤到坚果。

我使用Express 主要是因为我真的很喜欢JadeStylus — 我没有嫁给Express' 路由,如果我想做的事情只有通过Angular 的路由。

提前感谢任何人提供的任何帮助。请不要让我用谷歌搜索它,因为我有大约 26 页的紫色链接。 ;-)


1此解决方案依赖于 Angular 的 $httpBackend 模拟,目前尚不清楚如何使其与真实服务器通信。

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

【问题讨论】:

  • $cookie 是否没有解决您每次调用服务器的问题?如果您使用某些服务来管理范围内的变量,也许您不需要刷新页面。对不起,如果我误解了你的问题......
  • 我没有使用$cookie$cookieStore。 cookie(由服务器创建)被返回并存储在浏览器中,当我进行其他需要身份验证的 REST 调用时,我在调用中设置了withCredentials:true。这工作正常,它保持登录状态,并保护/隐藏路由,直到该用户登录我遇到问题。
  • @JonSamwell - 这太棒了,这个线程已经等了将近一年的答案。请将此列为答案并提供详细信息/代码的快速概述,我会将其标记为已接受。非常感谢。
  • @couzzi - 看看我的回答 - 我真的很高兴它有帮助

标签: javascript angularjs node.js restful-authentication angular-http-interceptors


【解决方案1】:

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

在我的控制器范围内,我观察 loginServiceInformation 并相应地填充模型的一些属性(以触发适当的 ng-show/hide)。关于路由,我使用的是Angular的内置路由,我只是有一个基于此处显示的loggedIn布尔值的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"]);

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>

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

注意我还没有在这里实现表单验证。诚然,对于 Angular 来说仍然很新鲜,所以欢迎任何指向这篇文章中的东西的指针。虽然这不能直接回答问题,因为它不是基于 RESTful 的实现,但我相信同样可以适应 $resources,因为它是建立在 $http 调用之上的。

【讨论】:

  • 这看起来很有希望。我会在早上试一试。谢谢!然而,有一个问题——你能详细说明这一点吗?:我在 DOM 中使用 ng-controller 定义了登录控制器,这样我就可以根据 loggedIn 变量更改页面的正文区域。
  • 是的,我实际上最终在帖子底部的那部分进行了编辑和粘贴,“Base HTML”是我所指的部分,在 body 标签中我有“NavigationCtrl”处理导航,是我的“顶级控制器”。然后您可以看到 LoginCtrl 是在我的“outerContainer”上定义的,它是包含其他所有内容的 div。这样我就可以在任何子 DOM 元素中使用它的作用域变量(在这种情况下基本上是任何地方)。
【解决方案2】:

我为UserApp 写了一个AngularJS module,它几乎可以满足您的所有要求。你可以:

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

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

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

如果您想尝试一下 UserApp,请拨打course on 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>
    
  • 带有错误处理的注册表单:

    <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>
    
  • 如何指定哪些路由应该是公开的,哪些路由是登录表单:

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

    .otherwise() 路由应设置为您希望用户在登录后重定向到的位置。示例:

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

  • 退出链接:

    &lt;a href="#" ua-logout&gt;Log Out&lt;/a&gt;

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

  • 访问用户属性:

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

    或者在模板中:&lt;span&gt;{{ user.email }}&lt;/span&gt;

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

    &lt;div ng-show="user.authorized"&gt;Welcome {{ user.first_name }}!&lt;/div&gt;

  • 根据权限显示元素:

    &lt;div ua-has-permission="admin"&gt;You are an admin&lt;/div&gt;

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

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

【讨论】:

  • 这是一个付费解决方案,不是吗?
  • 我建议,如果您打算外包您的用户身份验证,您应该希望它是一种付费解决方案(这意味着具有法律约束力)。是否要将应用程序的这样一个关键组件委托给第三方是另一回事......
  • UserApp 现在似乎已经死了
【解决方案3】:

这摘自我关于 url 路由授权和元素安全 here 的博客文章,但我将简要总结要点 :-)

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

关于 Angular 中安全性的主要关注点是路由安全,幸运的是,在 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'
        });

整个方法的重点是授权服务,该服务基本上会检查用户是否具有所需的权限。该服务将关注点从该解决方案的其他部分中抽象出来,与用户及其在登录期间从服务器检索到的实际权限有关。虽然代码非常冗长,但在我的博客文章中进行了充分解释。但是,它主要处理权限检查和两种授权模式。第一个是用户必须至少拥有一个已定义的权限,第二个是用户必须拥有所有已定义的权限。

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;
};

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

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()',因为它用我们将他们重定向到的路线替换了当前路线(他们无权查看的路线)。这会阻止任何然后导航回未经授权的路线。

现在我们可以拦截路线,我们可以做很多很酷的事情,包括redirecting after a login,如果用户登陆了他们需要登录的路线。

解决方案的第二部分是能够根据权限向用户隐藏/显示 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>

阅读我的full blog post,了解更详细的方法概述。

【讨论】:

    【解决方案4】:

    我创建了一个 github 存储库,基本上总结了这篇文章:https://medium.com/opinionated-angularjs/techniques-for-authentication-in-angularjs-applications-7bbf0346acec

    ng-login Github repo

    Plunker

    我会尽量解释清楚,希望对你们中的一些人有所帮助:

    (1) app.js:在应用定义中创建身份验证常量

    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) Auth Service: 以下所有功能均在 auth.js 服务中实现。 $http 服务用于与服务器通信以进行身份​​验证过程。还包含授权功能,即是否允许用户执行特定操作。

    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) 父控制器: 将其视为应用程序的“主要”功能,所有控制器都继承自此控制器,它是此应用程序身份验证的支柱。

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

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

    a) 在 ui 路由器的 $stateProvider 服务上添加允许访问每个路由的角色数据,如下所示(同样适用于 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') 上添加功能以防止用户未授权时状态更改。

    $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 请求后,此拦截器都会检查状态码,如果返回以下状态码之一,则广播事件以强制用户重新登录。

    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);
            }
        };
    } ]);
    

    P.S. 通过添加包含在directives.js 中的指令,可以轻松避免第一篇文章中所述的表单数据自动填充错误。

    P.S.2 用户可以轻松调整此代码,以允许查看不同的路线,或显示不应该显示的内容。逻辑必须在服务器端实现,这只是在您的 ng-app 上正确显示内容的一种方式。

    【讨论】:

      猜你喜欢
      • 2011-11-28
      • 2011-10-02
      • 2014-11-15
      • 1970-01-01
      • 2016-03-15
      • 2012-09-19
      • 2021-02-16
      • 2015-06-06
      • 2012-03-14
      相关资源
      最近更新 更多