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

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

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

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

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

[javascript]angular.module(‘auth’, [‘ngCookies’, ‘restangular’]);[/javascript]

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

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

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

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

[javascript]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();
};
});[/javascript]

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

[javascript]$http.defaults.headers.common[‘X-Token’] = token;[/javascript]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

return $q.reject(rejection);
};
});
[/javascript]

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

[javascript]
app.config(function($httpProvider) {
$httpProvider.interceptors.push(‘authRejector’);
});
[/javascript]

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

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

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

}
[/javascript]

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

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

[javascript]
.state(‘users’, {})
[/javascript]

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

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

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

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

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

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

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

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