API-интерфейс AngularJS Authentication + RESTful

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

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

В ореховой оболочке мне нужно

  • Вход через POST от http://client.foo до http://api.foo/login
  • Имейте "вход в систему" ​​GUI/компонентное состояние для пользователя, который предоставляет маршрут 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,
   ...
}

Что я пробовал

Почему я чувствую, что схожу с ума

  • Кажется, что каждый учебник опирается на некоторую базу данных (много монго, couch, PHP + MySQL, ad infinitum), и никто не полагается исключительно на связь с RESTful API для сохранения состояния входа в систему. После входа в систему дополнительные POST/GET отправляются с withCredentials:true, так что не проблема
  • Я не могу найти ЛЮБЫЕ примеры/учебники/репозиции, которые делают Angular + REST + Auth, без использования бэкэнд-языка.

Я не слишком горжусь

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

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

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


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

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

Ответ 1

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

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

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

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

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

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>

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

Ответ 2

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

  • Измените модуль и добавьте функции в свой собственный API или
  • Используйте модуль вместе с 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>

  • Показать элемент на основе разрешений:

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

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

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

Ответ 3

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

В пределах области моего контроллера я смотрю loginServiceInformation и заполняю некоторые свойства модели соответственно (для запуска соответствующего ng-show/hide). Что касается маршрутизации, я использую Angular, встроенный в маршрутизацию, и у меня просто есть ng-hide на основе логического логина, показанного здесь, в нем отображается текст для запроса логина, или же div с атрибутом ng-view (так что если нет вы вошли в систему сразу после входа в систему, вы находитесь на правильной странице, в настоящее время я загружаю данные для всех просмотров, но я считаю, что при необходимости это может быть более избирательным)

//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, я считаю, что то же самое можно адаптировать к $ресурсам, поскольку оно построено поверх вызовов $http.

Ответ 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 router $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. Ошибка с автозаполнением данных формы, как указано в 1-й статье, можно легко избежать, добавив директиву, включенную в директивы .js.

P.S.2 Этот код может быть легко изменен пользователем, чтобы можно было видеть разные маршруты или отображать содержимое, которое не предназначалось для отображения. Логика ДОЛЖНА быть реализована на стороне сервера, это всего лишь способ правильно показать вещи в вашем ng-приложении.