Авторизация AngularJS. Right way.

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

UPD: Статья обновлена и дополнена 2017-02-21

Отдельный модуль

Так как я сторонник модульного подхода и считаю, что именно это правильный способ использования AngualrJS, то весь функционал связанный с авторизацией я сложил в отдельный модуль auth. К нему подключаю модуль управления кукисами и модуль организации удобной работы с REST(restangular):

angular.module('auth', ['ngCookies', 'restangular']);

Сервис авторизации

Понятное дело, что какой бы вы самокат не делали, без сервиса не обойтись. Что в нем должно находиться? Делаем REST/token вариант авторизации, поэтому в сервисе будет следующий функционал:

  • авторизация по логину (асинхронная операция: POST запрос и получение/сохранения токена)
  • авторизация по токену (асинхронная операция: GET запрос и получение информации по пользователю)
  • проверка статуса (синхронная операция)
  • логаут (зачистка данных авторизации – токена)

Вот упрощенная версия кода(выкинул обработку ошибок):

angular.module('auth')
  .service('AuthService', function($cookies, $http, Restangular) {
    'use strict';

    var self = this;
    this.status = {
      authorized: false,
    };

    this.loginByCredentials = function(username, password) {
      return Restangular.all('sessions').post({ email: username, password: password })
        .then(function(response) {
          return self.loginByToken(response.contents);
        });
    };

    this.loginByToken = function(token) {
      $http.defaults.headers.common['X-Token'] = token;

      return Restangular.all('sessions').get(token)
        .then(function(response) {
          $cookies.accessToken = token;
          self.status.authorized = true;
          return response;
        });
    };

    this.logout = function() {
      self.status.authorized = false;
      $cookies.accessToken = '';

      Restangular.all('sessions').remove();
    };
  });

Приходится подключать $http сервис для вот этого хака:

$http.defaults.headers.common['X-Token'] = token;

– по другому в Restangular не получается динамически задать хедер, в котором мы хотим отправлять токен авторизации.

(если у вас есть другое решение данной ситуации – пожалуйста поделитесь)

Страница авторизации

Если вы используете стандартный ангуляровский роутер, тут может возникнуть вопрос: Как для авторизации показывать другой лэйаут?

Решение подсказал Валентин Шибанов, который предложил завернуть все содержимое(в том числе ui-view) в ng-if, который бы в зависимости от того, авторизираван пользователь или нет, показывал бы либо содержимое с меню либо формочку авторизации.

Итого имеем(опять очень упрощенная версия):

<div ng-if="isAutorized">
    <menu></menu>
    <ui-view></ui-vew>
<div ng-if="!isAutorized">
    <login></login>
</div>

Плюшки с интерсепторами

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

Момент определения состояния авторизации и ограничение доступа

Давайте разберемся, в какой момент мы должны/можем проверять состояние авторизации. Все варианты можно условно разбить на 2:

  • ответ API (мы узнали от сервера)
  • изменение состояния (перешли на другую страничку)
    • событие изменение состояния
    • резолв конкретного стейта

Давайте посмотрим на все случаи.

Ответ от API и интерсептор для обратки ошибки

Про интерсепторы можно более подробно почитать тут. Мы с вами реализуем один из них интесептор ошибки ответа:

app.service('authRejector', function($q) {
    this.responseError = (rejection) => {
      if (rejection.status === 401) {
        //делаем какие-то действия для пользователя без авторизации
      }

      return $q.reject(rejection);
    };
  });

и потом подключаем наш интерсептор к остальным:

  app.config(function($httpProvider) {
    $httpProvider.interceptors.push('authRejector');
  });

Изменение стейта

Навесить хук в событие ui-router можно с помощью сервиса $transitions:

$transitions.onEnter({ to: 'stateName' }, function($state$, $transition$) {
  if(!AuthService.status.authorized){
    //делаем какие-то действия для пользователя без авторизации
    return $q.reject()
  }
  
}

Запрет доступа к страница с помощью резолв

Давайте предлополжим, что у нас есть стейт users:

 .state('users', {})

к которому мы хотим ограничить доступ. Для этого добавим резолв с использованием все того же сервиса авторизации:

        .state('users', {
          resolve: {
            auth: function($q, AuthService) {
              if(!AuthService.status.authorized) {
                $q.reject();
                alert('Вы должны авторизироваться!');
              }
            }
          }
        })

Проверка авторизации при переходе на другую страницу

В ui-router мы можем навесить хук на событие перехода на какой либо стейт с помощью сервиса $transitions:

$transitions.onEnter({ to: 'stateName' }, function($state$, $transition$) {
  if(!AuthService.status.authorized){
    //делаем какие-то действия для пользователя без авторизации
    return $q.reject()
  }
}

Но у приведем сам логин компонент для полноты примера.

Пример логин компонента

app.component('login', {
  controller: function(AuthService){
    this.login = function(login, password)
    AuthService.loginByCredentials(login, password).catch(function(){
      //выводим ошибку авторизации
    });
  }
});