Учебник по JSON Web Token: пример в Laravel и AngularJS
Опубликовано: 2022-03-11С ростом популярности одностраничных приложений, мобильных приложений и сервисов RESTful API способы написания серверного кода веб-разработчиками значительно изменились. Благодаря таким технологиям, как AngularJS и BackboneJS, мы больше не тратим много времени на создание разметки, вместо этого мы создаем API, которые потребляют наши клиентские приложения. Наш бэкенд больше связан с бизнес-логикой и данными, а логика представления перемещена исключительно во внешний интерфейс или мобильные приложения. Эти изменения привели к появлению новых способов реализации аутентификации в современных приложениях.
Аутентификация — одна из важнейших частей любого веб-приложения. В течение десятилетий файлы cookie и аутентификация на основе сервера были самым простым решением. Однако обработка аутентификации в современных мобильных и одностраничных приложениях может быть сложной и требует лучшего подхода. Наиболее известными решениями проблем аутентификации для API являются OAuth 2.0 и JSON Web Token (JWT).
Прежде чем мы перейдем к этому руководству по JSON Web Token, что такое JWT?
Что такое веб-токен JSON?
Веб-токен JSON используется для отправки информации, которую можно проверить и которой можно доверять с помощью цифровой подписи. Он состоит из компактного и безопасного для URL-адреса объекта JSON, который криптографически подписан для проверки его подлинности, а также может быть зашифрован, если полезная нагрузка содержит конфиденциальную информацию.
Из-за своей компактной структуры JWT обычно используется в заголовках Authorization
HTTP или параметрах запроса URL.
Структура веб-токена JSON
JWT представлен как последовательность значений в кодировке base64url, разделенных символами точки.
Вот пример токена JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0 . yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw
Заголовок
Заголовок содержит метаданные для токена и минимально содержит тип подписи и алгоритм шифрования. (Вы можете использовать средство форматирования JSON для предварительной настройки объекта JSON.)
Пример заголовка
{ "alg": "HS256", "typ": "JWT" }
Этот пример заголовка JWT объявляет, что закодированный объект является веб-токеном JSON и что он подписан с использованием алгоритма HMAC SHA-256.
Как только это закодировано в base64, у нас есть первая часть нашего JWT.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Полезная нагрузка (претензии)
В контексте JWT утверждение может быть определено как утверждение об объекте (обычно о пользователе), а также дополнительные метаданные о самом маркере. Утверждение содержит информацию, которую мы хотим передать, и которую сервер может использовать для правильной обработки аутентификации JSON Web Token. Мы можем предоставить несколько претензий; к ним относятся имена зарегистрированных требований, имена общедоступных требований и имена частных требований.
Зарегистрированные претензии JWT
Это утверждения, зарегистрированные в реестре утверждений веб-токенов IANA JSON. Эти утверждения JWT не предназначены для обязательного использования, а служат отправной точкой для набора полезных взаимодействующих утверждений.
Это включает:
- iss : Эмитент токена
- sub : Тема токена
- aud : аудитория токена
- exp : время истечения срока действия JWT, определенное во времени Unix.
- nbf : время «Не раньше», определяющее время, до которого JWT не должен приниматься в обработку.
- iat : «Выпущено в» время в Unix-времени, когда был выпущен токен.
- jti : утверждение JWT ID предоставляет уникальный идентификатор JWT.
Публичные претензии
Публичные утверждения должны иметь устойчивые к коллизиям имена. Делая имя URI или URN, можно избежать конфликтов имен для JWT, где отправитель и получатель не являются частью закрытой сети.
Примером имени общедоступного утверждения может быть: https://www.toptal.com/jwt_claims/is_admin
, и рекомендуется поместить в это место файл с описанием утверждения, чтобы его можно было разыменовать для документации.
Частные претензии
Частные имена утверждений могут использоваться в местах, где обмен JWT происходит только в закрытой среде между известными системами, например внутри предприятия. Это утверждения, которые мы можем определить сами, например идентификаторы пользователей, роли пользователей или любую другую информацию.
Использование имен утверждений, которые могут иметь конфликтующие семантические значения за пределами закрытой или частной системы, может привести к конфликтам, поэтому используйте их с осторожностью.
Важно отметить, что мы хотим, чтобы веб-токен был как можно меньше, поэтому используйте только необходимые данные внутри общедоступных и частных требований.
Пример полезной нагрузки JWT
{ "iss": "toptal.com", "exp": 1426420800, "https://www.toptal.com/jwt_claims/is_admin": true, "company": "Toptal", "awesome": true }
В этом примере полезная нагрузка содержит два зарегистрированных утверждения, одно общедоступное утверждение и два частных утверждения. Как только он закодирован в base64, у нас есть вторая часть нашего JWT.
eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0
Подпись
Стандарт JWT следует спецификации веб-подписи JSON (JWS) для создания окончательного подписанного токена. Он генерируется путем объединения закодированного заголовка JWT и закодированной полезной нагрузки JWT и подписания с использованием надежного алгоритма шифрования, такого как HMAC SHA-256. Секретный ключ подписи хранится на сервере, поэтому он сможет проверять существующие токены и подписывать новые.
$encodedContent = base64UrlEncode(header) + "." + base64UrlEncode(payload); $signature = hashHmacSHA256($encodedContent);
Это дает нам последнюю часть нашего JWT.
yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw
Безопасность и шифрование JWT
Крайне важно использовать TLS/SSL в сочетании с JWT, чтобы предотвратить атаки «человек посередине». В большинстве случаев этого будет достаточно для шифрования полезной нагрузки JWT, если она содержит конфиденциальную информацию. Однако, если мы хотим добавить дополнительный уровень защиты, мы можем зашифровать саму полезную нагрузку JWT, используя спецификацию JSON Web Encryption (JWE).
Конечно, если мы хотим избежать дополнительных накладных расходов, связанных с использованием JWE, другой вариант — просто хранить конфиденциальную информацию в нашей базе данных и использовать наш токен для дополнительных вызовов API к серверу всякий раз, когда нам нужен доступ к конфиденциальным данным.
Зачем нужны веб-токены?
Прежде чем мы сможем увидеть все преимущества использования аутентификации JWT, мы должны взглянуть на то, как аутентификация выполнялась в прошлом.
Аутентификация на основе сервера
Поскольку протокол HTTP не имеет состояния, необходим механизм хранения информации о пользователе и способ аутентификации пользователя при каждом последующем запросе после входа в систему. Большинство веб-сайтов используют файлы cookie для хранения идентификатора сеанса пользователя.
Как это работает
Браузер отправляет запрос POST на сервер, который содержит идентификатор пользователя и пароль. Сервер отвечает файлом cookie, который устанавливается в браузере пользователя и включает идентификатор сеанса для идентификации пользователя.
При каждом последующем запросе сервер должен найти этот сеанс и десериализовать его, поскольку пользовательские данные хранятся на сервере.
Недостатки аутентификации на основе сервера
Трудно масштабировать : серверу необходимо создать сеанс для пользователя и сохранить его где-то на сервере. Это можно сделать в памяти или в базе данных. Если у нас распределенная система, мы должны убедиться, что используем отдельное хранилище сеансов, не связанное с сервером приложений.
Совместное использование запросов между источниками (CORS) : при использовании вызовов AJAX для извлечения ресурса из другого домена («между источниками») мы можем столкнуться с проблемами с запрещенными запросами, поскольку по умолчанию HTTP-запросы не включают файлы cookie для перекрестного доступа. запросы происхождения.
Связь с веб-фреймворком : при использовании аутентификации на основе сервера мы привязаны к схеме аутентификации нашего фреймворка. Очень сложно или даже невозможно обмениваться данными сеанса между разными веб-фреймворками, написанными на разных языках программирования.
Аутентификация на основе токенов
Аутентификация на основе токена/JWT не имеет состояния, поэтому нет необходимости хранить информацию о пользователе в сеансе. Это дает нам возможность масштабировать наше приложение, не беспокоясь о том, где пользователь вошел в систему. Мы можем легко использовать тот же токен для получения защищенного ресурса из домена, отличного от того, в который мы вошли.
Как работают веб-токены JSON
Браузер или мобильный клиент отправляет запрос на сервер аутентификации, содержащий информацию для входа пользователя. Сервер аутентификации создает новый токен доступа JWT и возвращает его клиенту. При каждом запросе к ограниченному ресурсу клиент отправляет токен доступа в строке запроса или заголовке Authorization
. Затем сервер проверяет токен и, если он действителен, возвращает защищенный ресурс клиенту.
Сервер аутентификации может подписать токен, используя любой безопасный метод подписи. Например, алгоритм с симметричным ключом, такой как HMAC SHA-256, можно использовать, если есть безопасный канал для обмена секретным ключом между всеми сторонами. В качестве альтернативы можно использовать асимметричную систему с открытым ключом, такую как RSA, что устраняет необходимость в дальнейшем совместном использовании ключей.
Преимущества аутентификации на основе токенов
Без сохранения состояния, легче масштабировать : токен содержит всю информацию для идентификации пользователя, что устраняет необходимость в состоянии сеанса. Если мы используем балансировщик нагрузки, мы можем передать пользователя на любой сервер, а не привязываться к тому же серверу, на котором мы вошли в систему.
Повторное использование : у нас может быть много отдельных серверов, работающих на разных платформах и доменах, повторно использующих один и тот же токен для аутентификации пользователя. Легко создать приложение, которое разделяет разрешения с другим приложением.
Безопасность JWT : поскольку мы не используем файлы cookie, нам не нужно защищаться от атак с подделкой межсайтовых запросов (CSRF). Мы по-прежнему должны шифровать наши токены с помощью JWE, если нам нужно поместить в них какую-либо конфиденциальную информацию, и передавать наши токены по HTTPS, чтобы предотвратить атаки «человек посередине».
Производительность : нет поиска на стороне сервера для поиска и десериализации сеанса по каждому запросу. Единственное, что нам нужно сделать, это вычислить HMAC SHA-256 для проверки токена и анализа его содержимого.
Пример веб-токена JSON с использованием Laravel 5 и AngularJS
В этом руководстве по JWT я собираюсь продемонстрировать, как реализовать базовую аутентификацию с использованием веб-токенов JSON в двух популярных веб-технологиях: Laravel 5 для серверного кода и AngularJS для примера одностраничного приложения (SPA). (Вы можете найти всю демонстрацию здесь и исходный код в этом репозитории GitHub, чтобы вы могли следовать руководству.)
В этом примере веб-токена JSON не будет использоваться какое-либо шифрование для обеспечения конфиденциальности информации, передаваемой в заявках. На практике это часто нормально, поскольку TLS/SSL шифрует запрос. Однако, если токен будет содержать конфиденциальную информацию, такую как номер социального страхования пользователя, его также следует зашифровать с помощью JWE.
Пример бэкенда Laravel
Мы будем использовать Laravel для обработки регистрации пользователей, сохранения пользовательских данных в базе данных и предоставления некоторых ограниченных данных, которые требуют аутентификации для использования приложением Angular. Мы также создадим пример субдомена API для имитации совместного использования ресурсов между источниками (CORS).
Установка и начальная загрузка проекта
Чтобы использовать Laravel, нам нужно установить менеджер пакетов Composer на нашу машину. При разработке в Laravel я рекомендую использовать предварительно упакованную «коробку» Vagrant Laravel Homestead. Он предоставляет нам полную среду разработки независимо от нашей операционной системы.
Самый простой способ загрузить наше приложение JWT Laravel — использовать пакет Composer Laravel Installer.
composer global require "laravel/installer=~1.1"
Теперь мы все готовы создать новый проект Laravel, запустив laravel new jwt
.
По любым вопросам об этом процессе обращайтесь к официальной документации Laravel.
После того, как мы создали базовое приложение Laravel 5, нам нужно настроить наш Homestead.yaml
, который настроит сопоставление папок и конфигурацию доменов для нашей локальной среды.
Пример файла Homestead.yaml
:
--- ip: "192.168.10.10" memory: 2048 cpus: 1 authorize: /Users/ttkalec/.ssh/public.psk keys: - /Users/ttkalec/.ssh/private.ppk folders: - map: /coding/jwt to: /home/vagrant/coding/jwt sites: - map: jwt.dev to: /home/vagrant/coding/jwt/public - map: api.jwt.dev to: /home/vagrant/coding/jwt/public variables: - key: APP_ENV value: local
После того, как мы загрузили нашу коробку Vagrant с помощью команды vagrant up
и вошли в нее с помощью vagrant ssh
, мы переходим в ранее определенный каталог проекта. В приведенном выше примере это будет /home/vagrant/coding/jwt
. Теперь мы можем запустить команду php artisan migrate
, чтобы создать необходимые пользовательские таблицы в нашей базе данных.
Установка зависимостей Composer
К счастью, есть сообщество разработчиков, работающих над Laravel и поддерживающих множество отличных пакетов, которые мы можем использовать повторно и расширять наше приложение. В этом примере мы будем использовать tymon/jwt-auth
от Шона Таймона для обработки токенов на стороне сервера и barryvdh/laravel-cors
от Barry vd. Heuvel для обработки CORS.
jwt-аутентификация
Требуйте tymon/jwt-auth
в нашем composer.json
и обновите наши зависимости.
composer require tymon/jwt-auth 0.5.*
Добавьте JWTAuthServiceProvider
в наш массив провайдеров app/config/app.php
.
'Tymon\JWTAuth\Providers\JWTAuthServiceProvider'
Затем в файле app/config/app.php
под массивом aliases
мы добавляем фасад JWTAuth
.
'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth'
Наконец, мы хотим опубликовать конфигурацию пакета, используя следующую команду: php artisan config:publish tymon/jwt-auth
Веб-токены JSON шифруются с помощью секретного ключа. Мы можем сгенерировать этот ключ с помощью команды php artisan jwt:generate
. Он будет помещен в наш файл config/jwt.php
. Однако в производственной среде мы никогда не хотим, чтобы наши пароли или ключи API находились в файлах конфигурации. Вместо этого мы должны поместить их в переменные среды сервера и ссылаться на них в файле конфигурации с помощью функции env
. Например:
'secret' => env('JWT_SECRET')
Мы можем узнать больше об этом пакете и всех его настройках конфигурации на Github.
laravel-cors
Требуйте barryvdh/laravel-cors
в нашем composer.json
и обновите наши зависимости.
composer require barryvdh/laravel-cors 0.4.x@dev
Добавьте CorsServiceProvider
в наш массив поставщиков app/config/app.php
.
'Barryvdh\Cors\CorsServiceProvider'
Затем добавьте промежуточное ПО в наше app/Http/Kernel.php
.
'Barryvdh\Cors\Middleware\HandleCors'
Опубликуйте конфигурацию в локальном файле config/cors.php
с помощью команды php artisan vendor:publish
.

Пример конфигурации файла cors.php
:
return [ 'defaults' => [ 'supportsCredentials' => false, 'allowedOrigins' => [], 'allowedHeaders' => [], 'allowedMethods' => [], 'exposedHeaders' => [], 'maxAge' => 0, 'hosts' => [], ], 'paths' => [ 'v1/*' => [ 'allowedOrigins' => ['*'], 'allowedHeaders' => ['*'], 'allowedMethods' => ['*'], 'maxAge' => 3600, ], ], ];
Маршрутизация и обработка HTTP-запросов
Для краткости я буду помещать весь свой код в файл route.php, который отвечает за маршрутизацию Laravel и делегирование запросов контроллерам. Обычно мы создаем выделенные контроллеры для обработки всех наших HTTP-запросов и сохраняем модульность и чистоту нашего кода.
Мы загрузим наше представление AngularJS SPA, используя
Route::get('/', function () { return view('spa'); });
Регистрация пользователя
Когда мы делаем POST
-запрос к /signup
с именем пользователя и паролем, мы пытаемся создать нового пользователя и сохранить его в базе данных. После создания пользователя создается JWT и возвращается через ответ JSON.
Route::post('/signup', function () { $credentials = Input::only('email', 'password'); try { $user = User::create($credentials); } catch (Exception $e) { return Response::json(['error' => 'User already exists.'], HttpResponse::HTTP_CONFLICT); } $token = JWTAuth::fromUser($user); return Response::json(compact('token')); });
Вход пользователя
Когда мы делаем POST
-запрос к /signin
с именем пользователя и паролем, мы проверяем, что пользователь существует, и возвращаем JWT через ответ JSON.
Route::post('/signin', function () { $credentials = Input::only('email', 'password'); if ( ! $token = JWTAuth::attempt($credentials)) { return Response::json(false, HttpResponse::HTTP_UNAUTHORIZED); } return Response::json(compact('token')); });
Получение ограниченного ресурса в том же домене
Как только пользователь вошел в систему, мы можем получить ограниченный ресурс. Я создал маршрут /restricted
, который имитирует ресурс, которому требуется аутентифицированный пользователь. Для этого заголовок Authorization
запроса или строка запроса должны предоставить JWT для проверки серверной частью.
Route::get('/restricted', [ 'before' => 'jwt-auth', function () { $token = JWTAuth::getToken(); $user = JWTAuth::toUser($token); return Response::json([ 'data' => [ 'email' => $user->email, 'registered_at' => $user->created_at->toDateTimeString() ] ]); } ]);
В этом примере я использую промежуточное ПО jwt-auth
, предоставленное в jwt-auth
, используя 'before' => 'jwt-auth'
. Это промежуточное ПО используется для фильтрации запроса и проверки токена JWT. Если токен недействителен, отсутствует или просрочен, промежуточное ПО выдаст исключение, которое мы сможем перехватить.
В Laravel 5 мы можем перехватывать исключения с помощью файла app/Exceptions/Handler.php
. Используя функцию render
, мы можем создавать HTTP-ответы на основе сгенерированного исключения.
public function render($request, Exception $e) { if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenInvalidException) { return response(['Token is invalid'], 401); } if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenExpiredException) { return response(['Token has expired'], 401); } return parent::render($request, $e); }
Если пользователь аутентифицирован и токен действителен, мы можем безопасно вернуть данные с ограниченным доступом во внешний интерфейс через JSON.
Получение ограниченных ресурсов из поддомена API
В следующем примере веб-токена JSON мы используем другой подход для проверки токена. Вместо использования промежуточного ПО jwt-auth
мы будем обрабатывать исключения вручную. Когда мы отправляем запрос POST
на сервер API api.jwt.dev/v1/restricted
, мы делаем запрос между источниками и должны включить CORS на бэкэнде. К счастью, мы уже настроили CORS в файле config/cors.php
.
Route::group(['domain' => 'api.jwt.dev', 'prefix' => 'v1'], function () { Route::get('/restricted', function () { try { JWTAuth::parseToken()->toUser(); } catch (Exception $e) { return Response::json(['error' => $e->getMessage()], HttpResponse::HTTP_UNAUTHORIZED); } return ['data' => 'This has come from a dedicated API subdomain with restricted access.']; }); });
Пример внешнего интерфейса AngularJS
Мы используем AngularJS в качестве внешнего интерфейса, полагаясь на вызовы API к внутреннему серверу аутентификации Laravel для аутентификации пользователя и демонстрационных данных, а также на сервер API для данных примеров из разных источников. Как только мы перейдем на домашнюю страницу нашего проекта, серверная часть будет обслуживать представление resources/views/spa.blade.php
, которое загрузит приложение Angular.
Вот структура папок приложения Angular:
public/ |-- css/ `-- bootstrap.superhero.min.css |-- lib/ |-- loading-bar.css |-- loading-bar.js `-- ngStorage.js |-- partials/ |-- home.html |-- restricted.html |-- signin.html `-- signup.html `-- scripts/ |-- app.js |-- controllers.js `-- services.js
Начальная загрузка приложения Angular
spa.blade.php
содержит самое необходимое для запуска приложения. Мы будем использовать Twitter Bootstrap для стилизации вместе с пользовательской темой от Bootswatch. Чтобы получить визуальную обратную связь при вызове AJAX, мы будем использовать скрипт angular-loading-bar, который перехватывает запросы XHR и создает панель загрузки. В разделе заголовка у нас есть следующие таблицы стилей:
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> <link rel="stylesheet" href="/css/bootstrap.superhero.min.css"> <link rel="stylesheet" href="/lib/loading-bar.css">
Нижний колонтитул нашей разметки содержит ссылки на библиотеки, а также наши пользовательские скрипты для модулей, контроллеров и сервисов Angular.
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular-route.min.js"></script> <script src="/lib/ngStorage.js"></script> <script src="/lib/loading-bar.js"></script> <script src="/scripts/app.js"></script> <script src="/scripts/controllers.js"></script> <script src="/scripts/services.js"></script> </body>
Мы используем библиотеку ngStorage
для AngularJS, чтобы сохранять токены в локальном хранилище браузера, чтобы мы могли отправлять их при каждом запросе через заголовок Authorization
.
В производственной среде, конечно, мы бы минимизировали и объединили все наши файлы сценариев и таблицы стилей, чтобы повысить производительность.
Я создал панель навигации с помощью Bootstrap, которая изменит видимость соответствующих ссылок в зависимости от статуса входа пользователя. Статус входа определяется наличием переменной token
в области действия контроллера.
<div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">JWT Angular example</a> </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav navbar-right"> <li data-ng-show="token"><a ng-href="#/restricted">Restricted area</a></li> <li data-ng-hide="token"><a ng-href="#/signin">Sign in</a></li> <li data-ng-hide="token"><a ng-href="#/signup">Sign up</a></li> <li data-ng-show="token"><a ng-click="logout()">Logout</a></li> </ul> </div>
Маршрутизация
У нас есть файл с именем app.js
, который отвечает за настройку всех наших интерфейсных маршрутов.
angular.module('app', [ 'ngStorage', 'ngRoute', 'angular-loading-bar' ]) .constant('urls', { BASE: 'http://jwt.dev:8000', BASE_API: 'http://api.jwt.dev:8000/v1' }) .config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) { $routeProvider. when('/', { templateUrl: 'partials/home.html', controller: 'HomeController' }). when('/signin', { templateUrl: 'partials/signin.html', controller: 'HomeController' }). when('/signup', { templateUrl: 'partials/signup.html', controller: 'HomeController' }). when('/restricted', { templateUrl: 'partials/restricted.html', controller: 'RestrictedController' }). otherwise({ redirectTo: '/' });
Здесь мы видим, что мы определили четыре маршрута, которые обрабатываются либо HomeController
, либо RestrictedController
. Каждый маршрут соответствует частичному представлению HTML. Мы также определили две константы, которые содержат URL-адреса для наших HTTP-запросов к серверной части.
Перехватчик запроса
Служба $http AngularJS позволяет нам взаимодействовать с серверной частью и выполнять HTTP-запросы. В нашем случае мы хотим перехватывать каждый HTTP-запрос и вставлять в него заголовок Authorization
, содержащий наш JWT, если пользователь аутентифицирован. Мы также можем использовать перехватчик для создания глобального обработчика ошибок HTTP. Вот пример нашего перехватчика, который внедряет токен, если он доступен в локальном хранилище браузера.
$httpProvider.interceptors.push(['$q', '$location', '$localStorage', function ($q, $location, $localStorage) { return { 'request': function (config) { config.headers = config.headers || {}; if ($localStorage.token) { config.headers.Authorization = 'Bearer ' + $localStorage.token; } return config; }, 'responseError': function (response) { if (response.status === 401 || response.status === 403) { $location.path('/signin'); } return $q.reject(response); } }; }]);
Контроллеры
В файле controllers.js
мы определили два контроллера для нашего приложения: HomeController
и RestrictedController
. HomeController
обрабатывает функции входа, регистрации и выхода из системы. Он передает данные имени пользователя и пароля из форм Auth
и регистрации в службу аутентификации, которая отправляет HTTP-запросы на серверную часть. Затем он сохраняет токен в локальном хранилище или показывает сообщение об ошибке, в зависимости от ответа серверной части.
angular.module('app') .controller('HomeController', ['$rootScope', '$scope', '$location', '$localStorage', 'Auth', function ($rootScope, $scope, $location, $localStorage, Auth) { function successAuth(res) { $localStorage.token = res.token; window.location = "/"; } $scope.signin = function () { var formData = { email: $scope.email, password: $scope.password }; Auth.signin(formData, successAuth, function () { $rootScope.error = 'Invalid credentials.'; }) }; $scope.signup = function () { var formData = { email: $scope.email, password: $scope.password }; Auth.signup(formData, successAuth, function () { $rootScope.error = 'Failed to signup'; }) }; $scope.logout = function () { Auth.logout(function () { window.location = "/" }); }; $scope.token = $localStorage.token; $scope.tokenClaims = Auth.getTokenClaims(); }])
RestrictedController
ведет себя так же, только он извлекает данные с помощью getRestrictedData
и getApiData
в службе Data
.
.controller('RestrictedController', ['$rootScope', '$scope', 'Data', function ($rootScope, $scope, Data) { Data.getRestrictedData(function (res) { $scope.data = res.data; }, function () { $rootScope.error = 'Failed to fetch restricted content.'; }); Data.getApiData(function (res) { $scope.api = res.data; }, function () { $rootScope.error = 'Failed to fetch restricted API content.'; }); }]);
Серверная часть отвечает за обслуживание данных с ограниченным доступом только в том случае, если пользователь прошел проверку подлинности. Это означает, что для ответа с ограниченными данными запрос этих данных должен содержать действительный JWT внутри своего заголовка Authorization
или строки запроса. Если это не так, сервер ответит кодом ошибки 401 Unauthorized.
Служба аутентификации
Служба аутентификации отвечает за вход и регистрацию HTTP-запросов к серверной части. Если запрос выполнен успешно, ответ содержит подписанный токен, который затем декодируется в кодировке base64, а вложенная информация об утверждениях токена сохраняется в переменной tokenClaims
. Это передается контроллеру через функцию getTokenClaims
.
angular.module('app') .factory('Auth', ['$http', '$localStorage', 'urls', function ($http, $localStorage, urls) { function urlBase64Decode(str) { var output = str.replace('-', '+').replace('_', '/'); switch (output.length % 4) { case 0: break; case 2: output += '=='; break; case 3: output += '='; break; default: throw 'Illegal base64url string!'; } return window.atob(output); } function getClaimsFromToken() { var token = $localStorage.token; var user = {}; if (typeof token !== 'undefined') { var encoded = token.split('.')[1]; user = JSON.parse(urlBase64Decode(encoded)); } return user; } var tokenClaims = getClaimsFromToken(); return { signup: function (data, success, error) { $http.post(urls.BASE + '/signup', data).success(success).error(error) }, signin: function (data, success, error) { $http.post(urls.BASE + '/signin', data).success(success).error(error) }, logout: function (success) { tokenClaims = {}; delete $localStorage.token; success(); }, getTokenClaims: function () { return tokenClaims; } }; } ]);
Служба данных
Это простая служба, которая отправляет запросы к серверу аутентификации, а также к серверу API для некоторых фиктивных ограниченных данных. Он делает запрос и делегирует успешные и ошибочные обратные вызовы контроллеру.
angular.module('app') .factory('Data', ['$http', 'urls', function ($http, urls) { return { getRestrictedData: function (success, error) { $http.get(urls.BASE + '/restricted').success(success).error(error) }, getApiData: function (success, error) { $http.get(urls.BASE_API + '/restricted').success(success).error(error) } }; } ]);
Помимо этого учебника по веб-токенам JSON
Аутентификация на основе токенов позволяет нам создавать несвязанные системы, которые не привязаны к конкретной схеме аутентификации. Маркер может быть сгенерирован где угодно и использован в любой системе, использующей тот же секретный ключ для подписи маркера. Они готовы для мобильных устройств и не требуют от нас использования файлов cookie.
Веб-токены JSON работают со всеми популярными языками программирования и быстро набирают популярность. Их поддерживают такие компании, как Google, Microsoft и Zendesk. Их стандартная спецификация от Internet Engineering Task Force (IETF) все еще находится в черновой версии и может немного измениться в будущем.
Еще многое предстоит рассказать о JWT, например, о том, как обрабатывать детали безопасности и обновлять токены по истечении срока их действия, но учебник JSON Web Token должен продемонстрировать базовое использование и, что более важно, преимущества использования JWT.
Дальнейшее чтение в блоге Toptal Engineering:
- Создание Node.js/TypeScript REST API, часть 3: MongoDB, аутентификация и автоматические тесты