Пользователь
0,0
рейтинг
21 ноября 2014 в 15:17

Разработка → Аутентификация на основе JSON Web Token в Django и AngularJS: часть вторая перевод

В первой части мы рассмотрели, что для формирования JSON Web Token необходимы: сериализаторы и представления.

Теперь мы создадим шаблоны и поработаем над сервисами для аутентификации и получения данных.

Bower, менеджер пакетов для web-приложений


Прежде чем перейдем к коду, давайте установим все необходимые зависимости. Для этого мы будем использовать Bower, он является идеальным инструментом для управления зависимостями web-приложений.

Предполагается что у вас уже установлен Node.js. Для установки bower просто выполните следующую команду:
$ npm install -g bower

Примечание: Возможно понадобятся права администратора.
Для того чтобы изменить каталог по умолчанию, в который bower будет устанавливать пакеты, в корне вашего проекта создайте файл с названием “.bowerrc ” и добавьте в него следующие строки:
{
    "directory": "static/bower_components"
}

Мы указали каталог “static”, чтобы эти компоненты были доступны в Django.

Выполните команду:
$ bower init 

и настройте предложенные параметры.
Теперь можно установить зависимости выполнив следующую команду:
$ bower install --save angular angular-route bootstrap jquery


Обработка шаблонов на стороне сервера


Когда установлены необходимые компоненты можно перейти к коду.
Помните файл templates/index.html, который мы создали в первой части. Пришло время заполнить его.
Замените содержимое templates/index.html на следующее:
Скрытый текст
<!DOCTYPE html>
<html ng-app="application">
  <head>
    <title>django-angular-token-auth</title>
    <base href="/" />
    <link rel="stylesheet" href="/static/bower_components/bootstrap/dist/css/bootstrap.css" />
  </head>

  <body>
    <div class="container-fluid">
      <div class="ng-view row"></div>
    </div>

    <script type="text/javascript" src="/static/bower_components/jquery/dist/jquery.js"></script>
    <script type="text/javascript" src="/static/bower_components/bootstrap/dist/js/bootstrap.js"></script>
    <script type="text/javascript" src="/static/bower_components/angular/angular.js"></script>
    <script type="text/javascript" src="/static/bower_components/angular-route/angular-route.js"></script>

    <script type="text/javascript" src="/static/javascripts/app.js"></script>
    <script type="text/javascript" src="/static/javascripts/app.config.js"></script>
    <script type="text/javascript" src="/static/javascripts/app.routes.js"></script>
  </body>
</html>


Благодаря использованию bootstrap стилей, страница будет выглядеть довольно красиво.
Так же для AngularJS мы включили тег <base/> внутри тега <head/>. Он необходим, поскольку мы будем использовать HTML 5 маршрутизацию.

Начало работы с AngularJS


Давайте пойдем дальше, создадим файл /static/javascripts/app.js и добавим в него:
angular.module('application', [
  'application.config',
  'application.routes'
]);

angular.module('application.config', []);
angular.module('application.routes', ['ngRoute']);

Здесь создаются AngularJS модули, которые мы будем использовать для настройки конфигурации и маршрутизации.
Создайте файл /static/javascripts/app.config.js. И заполните его следующим содержимым:
angular.module('application.config')
  .config(function ($httpProvider, $locationProvider) {
    $locationProvider.html5Mode(true).hashPrefix('!');
  });

Здесь мы хотим включить режим HTML 5 маршрутизации. Он устраняет использование символа # в URL, который должен использоваться для других целей. Это так же объясняет почему мы включили <base /> в <head />.
Примечание: Позже мы будем использовать $httpProvider, поэтому убедитесь, что он прописан в зависимостях.
Создайте файл /static/javascripts/app.routes.js и добавьте в него следующее:
angular.module('application.routes')
  .config(function ($routeProvider) {
    $routeProvider.when('/', {
      controller: 'IndexController',
      templateUrl: '/static/templates/static/index.html'
    });
  });


Создание сервиса Auth


Самое время начать реализацию Auth сервиса, который будет заниматься регистрацией, входом и выходом пользователей.
Начнем с создания файла /static/javascripts/auth/auth.module.js.
В этом файле настроим модули, которые мы будем использовать для аутентификации:
angular.module('application.auth', [
  'application.auth.controllers',
  'application.auth.interceptors',
  'application.auth.services'
]);

angular.module('application.auth.controllers', []);
angular.module('application.auth.interceptors', []);
angular.module('application.auth.services', []);

Вы наверное могли заметить, что автор большой поклонник модульной структуры при использовании AngularJS.
Мы просто создаем различные модули для контроллеров, перехватчиков, сервисов и добавляем их в модуль application.auth.
Не забудьте добавить auth.module.js в index.html
<script type="text/javascript" src="/static/javascripts/auth/auth.module.js"></script>

После того, как добавили auth.module.js, откройте /static/javascripts/app.js и включите модуль application.auth в зависимости нашего приложения.
Теперь давайте создадим сервис Auth. Создайте файл /static/javascripts/auth/services/auth.service.js и включите в него следующее:
Скрытый текст
angular.module('application.auth.services').
  service('Auth', function ($http, $location, $q, $window) {
    var Auth = {
      getToken: function () {
        return $window.localStorage.getItem('token');
      },

      setToken: function (token) {
        $window.localStorage.setItem('token', token);
      },

      deleteToken: function () {
        $window.localStorage.removeItem('token');
      }
    };

    return Auth;
  });


Мы создали некоторые методы, которые будем использовать с Auth сервисом. Здесь мы используем только одну зависимость $window. Остальные будут использоваться в других методах: login(), logout(), и register(). Давайте создадим их.
Добавьте функцию login() в сервис Auth, как здесь:
Скрытый текст
login: function (username, password) {
  var deferred = $q.defer();

  $http.post('/api/v1/auth/login/', {
    username: username, password: password
  }).success(function (response, status, headers, config) {
    if (response.token) {
      Auth.setToken(response.token);
    }

    deferred.resolve(response, status, headers, config);
  }).error(function (response, status, headers, config) {
    deferred.reject(response, status, headers, config);
  });

  return deferred.promise;
},


Здесь мы делаем простой AJAX запрос к одному из наших API, которое сделали в первой части. Если в ответе присутствует маркер, то он сохраняется, для последующих API вызовов.
Выход пользователя более простой. Из-за использования аутентификации на основе JWT, нам всего лишь нужно стереть маркер из хранилища. Добавьте функцию logout() в Auth сервис:
logout: function () {
  Auth.deleteToken();
  $window.location = '/';
},

Последний метод, который нужно добавить в Auth сервис это register(), он очень похож на login(). В нем мы делаем запрос к нашему API, для создания пользователя и затем вторым вызовом заходим в систему от имени этого пользователя.
Добавьте метод register() в Auth сервис:
Скрытый текст
register: function (user) {
  var deferred = $q.defer();

  $http.post('/api/v1/auth/register/', {
    user: user
  }).success(function (response, status, headers, config) {
    Auth.login(user.username, user.password).
      then(function (response, status, headers, config) {
        $window.location = '/';
      });

    deferred.resolve(response, status, headers, config);
  }).error(function (response, status, headers, config) {
    deferred.reject(response, status, headers, config);
  });

  return deferred.promise;
}


Сервис Auth готов, чтобы включить его в index.html:
<script type="text/javascript" src="/static/javascripts/auth/services/auth.service.js"></script>

Теперь у нас есть возможность зарегистрироваться, войти и выйти из системы.
Сейчас нам нужно добавить перехватчик для того чтобы он добавлял наш маркер в каждый AJAX запрос.
Создайте /static/javascripts/auth/interceptors/auth.interceptor.js и вставьте в него следующее:
Скрытый текст
angular.module('application.auth.interceptors')
  .service('AuthInterceptor', function ($injector, $location) {
    var AuthInterceptor = {
      request: function (config) {
        var Auth = $injector.get('Auth');
        var token = Auth.getToken();

        if (token) {
          config.headers['Authorization'] = 'JWT ' + token;
        }
        return config;
      },

      responseError: function (response) {
        if (response.status === 403) {
            $location.path('/login');
        }
        return response;
      }
    };
    return AuthInterceptor;
  });


Здесь мы сделали несколько вещей, давайте рассмотрим их.
В этом сервисе два основных метода: request и responseError. Метод request вызывается при каждом AJAX запросе, а метод responseError вызывается, когда AJAX запрос возвращает ошибку.
Ранее упоминалось, что мы добавляем маркер в каждый AJAX запрос. Это делается в методе request. Каждый раз, когда вызывается метод request, мы проверяем есть ли у нас маркер (метод Auth.getToken()) и если да то мы добавляем его в заголовок запроса. Если маркера нет, выполняется простой запрос.
Примечание: вы можете заметить, что здесь используется сервис $injector. Он необходим, потому что иногда из-за перехватчиков возникают циклические зависимости, которые в свою очередь вызывают ошибки. Использование $injector лучший способ получить сервис Auth, обойдя эту проблему.
В функции responseError мы проверяем ответ на наличие 403 ошибки. Эту ошибку бросает djangorestframework-jwt, когда нет маркера или когда его срок действия истек. В любом случае мы должны перенаправить пользователя на страницу входа в систему. Для этого не требуется обновление страницы.
Теперь, когда мы закончили с перехватчиком, нам нужно сделать две вещи: добавить его в index.html и app.config.js.
Откройте index.html и добавьте следующую строку:
<script type="text/javascript" src="/static/javascripts/auth/interceptors/auth.interceptor.js"></script>

Теперь откройте app.config.js и там где мы включали HTML 5 режим добавьте следующую строку:
$httpProvider.interceptors.push('AuthInterceptor');

Это добавит AuthInterceptor в список перехватчиков, которые используются сервисом $http. Каждый перехватчик вызывается при каждом запросе, поэтому их не должно быть слишком много.

Сервис для получения пользователей


Последнее, что мы сделаем во второй части это сервис для получения пользователей.
Этот сервис очень прост. На самом деле он нужен только для проверки работоспособности нашего приложения.
Создайте файл /static/javascripts/auth/services.users.service.js и добавьте в него следующее:
angular.module('application.auth.services')
  .service('Users', function ($http) {
    var Users = {
      all: function () {
        return $http.get('/api/v1/users/');
      }
    };
    return Users;
  });

Теперь добавьте его в Index.html под сервисом Auth:
<script type="text/javascript" src="/static/javascripts/auth/services/users.service.js"></script>


Завершая вторую часть


Во второй части мы рассмотрели сервисы и перехватчики, которые необходимы для построения системы аутентификации. Затронули механизм Django, который обрабатывает статические файлы. Осталось определить несколько контроллеров и шаблонов.
В третьей части мы поговорим о контроллерах и шаблонах. Это будет еще одна длинная статья.
Если вы заинтересованы в коде, можете проверить мой репозиторий на Github.

От переводчика
  • Автор сильно переусердствовал с модульностью, в приложении практически нечего нет, а файлов и модулей уже...
  • Третью часть переводить не вижу смысла, потому что тема использования JWT раскрыта.
  • Так же считаю излишним выносить js в отдельный шаблон и потом его подключать. В оригинале автор использует для этого templates/javascripts.html.
  • И еще, в оригинале автор использовал глобальный объект window (window.angular.module('application.auth.services')) хотя в репозитории уже лежит довольно красивый код.

Перевод: James Brewer
@d06pbiy_kot
карма
9,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (3)

  • +3
    Автор сильно переусердствовал с модульностью, в приложении практически нечего нет, а файлов и модулей уже...

    Кстати, на ангуляре часто так и пишут: один файл — одна сущность. Это снимает часть вопросов, связанных с поддержкой кода, его тестируемостью и повторным использованием. Например, недавно была статья habrahabr.ru/post/243565/. У автора тоже что-то подобное, только все вручную. Все-таки JS менее выразительный язык, чем питон, потому код имеет тенденцию скатываться в состояние неподдерживаемого «говнокода» быстрее. Отсюда, приходится строже относится к таким вещам, как модульность.

    Так же считаю излишним выносить js в отдельный шаблон и потом его подключать. В оригинале автор использует для этого templates/javascripts.html.

    Мы в коммерческой разработке используем не только bower, но и много — gulp для «компиляции» статики, пост-процесснига django-шаблонов, и т.п… С одной стороны, тащить в проект уровня «hello world!» инфраструктуру кажется перебором, но с другой — к хорошему быстро привыкаешь, так что потом сложно отказываться. :) Могу предположить, что в реальных проектах у них больше одного django-шаблона, куда подключается один и тот же набор скриптов. Отсюда такое решение.
    • 0
      Кстати, на ангуляре часто так и пишут: один файл — одна сущность. Это снимает часть вопросов, связанных с поддержкой кода, его тестируемостью и повторным использованием. Например, недавно была статья habrahabr.ru/post/243565/. У автора тоже что-то подобное, только все вручную.

      Полностью согласен, но во всем должно быть понимание, когда это нужно, а когда нет. Есть рекомендации и каждый выбирает для себя. IMHO в данном случае, поскольку этот проект не претендует на шаблон для angular генератора, можно было сделать структуру и попроще.

      Все-таки JS менее выразительный язык, чем питон, потому код имеет тенденцию скатываться в состояние неподдерживаемого «говнокода» быстрее. Отсюда, приходится строже относится к таким вещам, как модульность.

      Что то Я про выразительность не особо понял, причем тут она вообще. Да в JS нет модульности, но мы то здесь не на чистом JS пишем, а используем angular и этого хватает. (единственное чего действительно не хватает так это загрузки по требованию) А если захотеть, то и на python можно такое наго****ть. И что, неужели есть необходимость помещать RegisterController и LoginController в отдельные файлы, когда можно использовать один файл:
      controllers.js
      (function () {
        'use strict';
      
        angular
          .module('application.auth.controllers')
          .controller('LoginController', ['Auth', function (Auth) {
            var vm = this;
      
            vm.user = {};
            vm.login = function () {
              Auth.login(vm.username, vm.password);
            }
          }])
          .controller('RegisterController', ['Auth', function (Auth) {
            var vm = this;
        
            vm.register = function () {
              Auth.register(vm.username, vm.password, vm.email);
            }
          }
        ]);
      
      })();
      


      Мы в коммерческой разработке используем не только bower, но и много — gulp для «компиляции» статики, пост-процесснига django-шаблонов, и т.п… С одной стороны, тащить в проект уровня «hello world!» инфраструктуру кажется перебором, но с другой — к хорошему быстро привыкаешь, так что потом сложно отказываться. :) Могу предположить, что в реальных проектах у них больше одного django-шаблона, куда подключается один и тот же набор скриптов. Отсюда такое решение.


      А зачем мешать backend с fronten' ом? Если frontend с rest api, то он ну никак не должен зависеть от backend' a. И что будите делать со своим frontend' ом если по каким то причинам нужно будет поменять backend?
      А как вы тестируете frontend?

      P. S. Если Я не прав в каких то моментах, буду рад выслушать критику.
  • 0
    И еще, в оригинале автор использовал глобальный объект window (window.angular.module('application.auth.services')) хотя в репозитории уже лежит довольно красивый код.

    ну там и $q лишнее, $http уже промис возвращает

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.