Аутентификация AngularJS + RESTful API

угловая + RESTful клиентская связь w / API для маршрутизации Auth / (re)

это было рассмотрено в нескольких разных вопросах и в нескольких разных учебниках, но все предыдущие ресурсы, с которыми я столкнулся, не совсем попали в точку.

в ореховой скорлупе мне нужно

  • войти через сообщение от http://client.foo to http://api.foo/login
  • имейте "зарегистрированное" состояние GUI / component для пользователя, который предоставляет logout маршрут
  • быть в состоянии "обновление" интерфейса, когда пользователь выходит / выходит. это было самым разочаровывающим
  • защитите мои маршруты, чтобы проверить аутентифицированное состояние (если им это нужно) и перенаправить пользователя на страницу входа в систему соответственно

мои вопросы:

  • каждый раз, когда я перехожу на другую страницу, мне нужно позвонить в 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, ad infinitum), и никто не полагается исключительно на связь с RESTful API для сохранения зарегистрированных состояний. После входа в систему, дополнительные сообщения / получает отправляются с withCredentials:true, так что это не проблема
  • я не могу найти примеров / учебников / репо, которые делают Angular+REST+Auth, sans a язык бэкэнда.

я не слишком горд

по общему признанию, Я новичок в Angular, и не удивлюсь, если я подхожу к этому смешным образом; я был бы в восторге, если кто-то предложит альтернативу-даже если это суп-орехи.

я использую Express в основном потому, что я действительно люблю Jade и Stylus - Я не замужем за Express' маршрутизация и откажется от нее, если то, что я хочу сделать, возможно только с Angular маршрутизирующий.

заранее спасибо за любую помощь кто-нибудь может предоставить. И, пожалуйста, не просите Меня Google, потому что у меня около 26 страниц фиолетовых ссылок. ;-)


1это решение зависит от макета $httpBackend Angular, и неясно, как заставить его разговаривать с реальным сервером.

2это было ближе всего, но поскольку у меня есть существующий API, с которым мне нужно аутентифицироваться, я не мог использовать "localStrategy" паспорта, и это казалось безумие написать службу OAUTH...это только я намеревался использовать.

4 ответов


это взято из моего сообщения в блоге по авторизации маршрута url и безопасности элемента здесь но я кратко резюмирую основные моменты: -)

безопасность в веб-приложении frontend-это просто начальная мера, чтобы остановить Joe Public, однако любой пользователь с некоторыми веб-знаниями может обойти его, поэтому вы всегда должны иметь сервер безопасности.

основная забота о безопасности в 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 ()', поскольку это заменяет текущий маршрут (тот, на который у них нет прав) маршрутом, на который мы перенаправляем их. Это остановить любой тогда возвращаемся к несанкционированному маршруту.

теперь мы можем перехватывать маршруты мы можем сделать довольно много интересных вещей, в том числе перенаправление после входа в систему если пользователь приземлился на маршруте, для которого им нужно было войти в систему.

вторая часть решения может скрывать / показывать элемент пользовательского интерфейса пользователю в зависимости от прав. Это достигается с помощью простой директивы.

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>

читать мои полный пост в блоге для более подробного обзора подхода.


я написал модуль AngularJS на UserApp что делает почти все, что вы попросите. Можно:

  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>
    
  • форма регистрации с ошибкой обращение:

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

  • выйти из системы ссылка:

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

  • показать элемент на основе permissions:

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

и для аутентификации в ваших серверных службах просто используйте user.token() чтобы получить токен сеанса и отправить его с запросом AJAX. В конце используйте API для UserApp (если вы используете UserApp), чтобы проверить, является ли токен допустимым или нет.

Если вам нужна помощь, просто дайте мне знать :)


я не использую $resource, потому что я просто вручную создаю свои служебные вызовы для своего приложения. Тем не менее, я обработал login, имея службу, которая зависит от всех других служб, которые получают какие-то данные инициализации. При успешном входе в систему запускается инициализация всех служб.

В моей области контроллера я наблюдаю loginServiceInformation и заполняю некоторые свойства модели соответственно (чтобы вызвать соответствующий ng-show/hide). Что касается маршрутизации я использую угловой встроенный маршрутизации и я просто НГ-скрыть на основе сеанса в поле логическое значение, показанное здесь, он показывает текст запроса, войдите или другой 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>

базовый 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>

у меня есть контроллер входа, определенный с ng-контроллером выше в DOM, чтобы я мог изменить область тела моей страницы на основе переменной loggedIn.

Примечание я еще не реализовал проверку формы здесь. Также, по общему признанию, все еще довольно свежо для Angular, поэтому любые указатели на вещи в этом посте приветствуются. Хотя это не отвечает на вопрос напрямую, так как это не реализация на основе RESTful, я считаю, что то же самое можно адаптировать к $ resources поскольку он построен поверх $ http-вызовов.


Я создал РЕПО GitHub, суммируя эту статью в основном: https://medium.com/opinionated-angularjs/techniques-for-authentication-in-angularjs-applications-7bbf0346acec

ng-войти GitHub РЕПО

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.служба 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) добавьте данные ролей, разрешенных для доступа к каждому маршруту, на сервисе $stateProvider ui router, как показано ниже (то же самое может работать для 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) авт перехватчик: это реализовано, но не может быть проверено в области этого кода. После каждого запроса $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);
        }
    };
} ]);

П. С. ошибка с автозаполнением данных формы, как указано в 1-й статье, можно легко избежать, добавив директиву, которая включена в директивы.js.

П. С. 2 этот код может быть легко изменен пользователем, чтобы можно было видеть различные маршруты или отображать контент, который не должен отображаться. Логика должна быть реализована на стороне сервера, это просто способ показать все правильно на вашем ng-приложении.