Регистрация и авторизация в php с JSON Web Token | Only to top

Почему json web tokes лучше обычных токенов?

При переходе с обычных токенов на JWT мы получаем несколько преимуществ:

  1. JWT – открытый стандарт. Это означает, что что все реализации JWT должны быть довольно похожими, что является преимуществом при работе с разными языками и технологиями. Обычные токены имеют более свободную форму, что позволяет разработчику решать, как лучше всего реализовывать токены.

  2. JWT содержат информацию о пользователе, что удобно для клиентской стороны.

  3. Библиотеки здесь берут на себя основную тяжелую работу. Развертывание собственной системы аутентификации опасно, поэтому мы оставляем важные вещи проверенным «в боях» библиотекам, которым можем доверять.

Аутентификация, основанная на сессии

По умолчанию, Django использует сессии для аутентификации. Прежде чем идти дальше, нужно проговорить, что это значит, почему это важно, что такое аутентификация на основе токенов и что такое JSON Web Token Authentication (JWT для краткости), и что из всего этого мы будем использовать далее в статье.

Вступление

Идентификация по JWT (JSON Web Token) — это довольно единообразный, согласованный механизм авторизации и аутентификации между сервером и клиентами. Преимущества JWT в том, что он позволяет нам меньше управлять состоянием и хорошо масштабируется. Неудивительно, что авторизация и аутентификация с его помощью все чаще используется в современных веб-приложениях.

При разработке приложений с JWT часто возникает вопрос: где и как рекомендуется хранить токен? Если мы разрабатываем веб-приложение, у нас есть два наиболее распространенных варианта:

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

Web Storage (localStorage/sessionStorage) доступен через JavaScript в том же домене. Это означает, что любой JavaScript код в вашем приложении имеет доступ к Web Storage, и это порождает уязвимость к cross-site scripting (XSS) атакам. Как механизм хранения Web Storage не предоставляет никаких способов обезопасить свои данные во время хранения и обмена.

Технологии

Для решения используем фреймворк Spring Boot и Spring Web, для него требуется:

  1. Java 8 ;

  2. Apache Maven

Авторизация и валидация будет выполнена силами Spring Security и JsonWebToken (JWT).Для уменьшения кода использую Lombok.

Создание API для регистрации пользователей

Нам нужно установить заголовки, чтобы файл для создания пользователей принимал только данные JSON

Создание приложения

Переходим к практике. Создаем Spring Boot приложение и реализуем простое REST API для получения данных пользователя и списка пользователей.

1 Создание Web-проекта

Создаем Maven-проект SpringBootSecurityRest. При инициализации, если вы это делаете через Intellij IDEA, добавьте Spring Boot DevTools, Lombok и Spring Web, иначе добавьте зависимости отдельно в pom-файле.

2 Конфигурация pom-xml

После развертывания проекта pom-файл должен выглядеть следующим образом:

  1. Должен быть указан parent-сегмент с подключенным spring-boot-starter-parent;

  2. И установлены зависимости spring-boot-starter-web, spring-boot-devtools и Lombok.

3 Создание ресурса REST

Разделим все классы на слои, создадим в папке com.springbootsecurityrest четыре новые папки:

Spring Security

Простенькое REST API написано и пока оно открыто для всех. Двигаемся дальше, теперь его необходимо защитить, а доступ открыть только авторизованным пользователям. Для этого воспользуемся Spring Security и JWT.

Spring Security это Java/JavaEE framework, предоставляющий механизмы построения систем аутентификации и авторизации, а также другие возможности обеспечения безопасности для корпоративных приложений, созданных с помощью Spring Framework.

JSON Web Token (JWT) — это открытый стандарт (RFC 7519) для создания токенов доступа, основанный на формате JSON. Как правило, используется для передачи данных для аутентификации в клиент-серверных приложениях. Токены создаются сервером, подписываются секретным ключом и передаются клиенту, который в дальнейшем использует данный токен для подтверждения своей личности.

Создание API для входа пользователей

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

Откроем файл api/login.php и поместим в него следующий код.

Мы сравним электронную почту пользователя и пароль из базы данных, поэтому нам нужно подключение к БД.

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

1 Подключаем зависимости

Добавляем новые зависимости в pom-файл.

2 Генерация и хранения токена

Начнем с генерации и хранения токена, для этого создадим папку security и в ней создаем класс JwtTokenRepository с имплементацией интерфейса CsrfTokenRepository (из пакета org.springframework.security.web.csrf).

Интерфейс указывает на необходимость реализовать три метода:

  1. Генерация токена в методе generateToken;

  2. Сохранения токена – saveToken;

  3. Получение токена – loadToken.

Генерируем токен силами Jwt, пример реализации метода.

3 Создание нового фильтра для SpringSecurity

Создаем новый класс JwtCsrfFilter, который является реализацией абстрактного класса OncePerRequestFilter (пакет org.springframework.web.filter). Класс будет выполнять валидацию токена и инициировать создание нового. Если обрабатываемый запрос относится к авторизации (путь /auth/login), то логика не выполняется и запрос отправляется далее для выполнения базовой авторизации.

6 Обработка ошибок

Что бы видеть ошибки авторизации или валидации токена, необходимо подготовить обработчик ошибок. Для этого создаем новый класс GlobalExceptionHandler в корне com.springbootsecurityrest, который является расширением класса ResponseEntityExceptionHandler с реализацией метода handleAuthenticationException.

Метод будет устанавливать статус ответа 401 (UNAUTHORIZED) и возвращать сообщение в формате ErrorInfo.

7 Настройка конфигурационного файла Spring Security.

Все данные подготовили и теперь необходимо настроить конфигурационный файл. В папке com.springbootsecurityrest создаем файл SpringSecurityConfig, который является реализацией абстрактного класса WebSecurityConfigurerAdapter пакета org.springframework.security.config.annotation.web.configuration. Помечаем класс двумя аннотациями: Configuration и EnableWebSecurity.

Создание API для валидации JWT

Приступим к наполнению файла api/validate_token.php.

Установим правильные заголовки. Этот файл вернет вывод в формате JSON и примет запросы от указанного URL.

<?php
// заголовки
header("Access-Control-Allow-Origin: http://localhost/rest-api-authentication-example/");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
 
// требуется для декодирования JWT
include_once "config/core.php";
include_once "libs/php-jwt-master/src/BeforeValidException.php";
include_once "libs/php-jwt-master/src/ExpiredException.php";
include_once "libs/php-jwt-master/src/SignatureInvalidException.php";
include_once "libs/php-jwt-master/src/JWT.php";
use FirebaseJWTJWT;
 
// получаем значение веб-токена JSON
$data = json_decode(file_get_contents("php://input"));

// получаем JWT
$jwt=isset($data->jwt) ? $data->jwt : "";

// если JWT не пуст
if($jwt) {
 
    // если декодирование выполнено успешно, показать данные пользователя
    try {
        // декодирование jwt
        $decoded = JWT::decode($jwt, $key, array("HS256"));
 
        // код ответа
        http_response_code(200);
 
        // показать детали
        echo json_encode(array(
            "message" => "Доступ разрешен.",
            "data" => $decoded->data
        ));
 
    }
 
    // если декодирование не удалось, это означает, что JWT является недействительным
    catch (Exception $e){
    
        // код ответа
        http_response_code(401);
    
        // сообщить пользователю отказано в доступе и показать сообщение об ошибке
        echo json_encode(array(
            "message" => "Доступ закрыт.",
            "error" => $e->getMessage()
        ));
    }
}
 
// показать сообщение об ошибке, если jwt пуст
else{
 
    // код ответа
    http_response_code(401);
 
    // сообщить пользователю что доступ запрещен
    echo json_encode(array("message" => "Доступ запрещён."));
}
?>

Создание интерфейса для регистрации пользователей

Мы будем использовать API, которые мы создали ранее. Все необходимые коды будут в одном файле index.html с использованием HTML, CSS и JavaScript.

Откройте файл index.html в нашей папке authentication-jwt. Поместите следующий код:

Создадим файл custom.css который мы подключили в шапке, со следующим содержимым:

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

Если щёлкнуть на меню «Вход» на панели навигации, отобразится форма входа.

Best practices в jwt

И в завершении приведу best practices при использовании этого вида токенов, которых следует придерживаться:

Json web tokens

Учитывая указанные проблемы, в сообществе разработчиков много лет назад появилась идея использовать некую строку, которую с одной стороны невозможно подделать, с другой — которую каждый ресурсный сервер мог бы сам проверить на валидность. В качестве такой строки оказалось удобно использовать JWT — JSON Web Token.

JWT представляет собой строку из трех частей, разделенных точками. На иллюстрации ниже приведены значения каждой части:

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

Но никто не может прочитать записанное в третьей части — и соответственно никто не может подделать токен.

Если злоумышленник, например, укажет во второй части другое имя пользователя, то изменится и подпись в третьей части. Но поскольку у злоумышленника нет секретного ключа, то он не сможет правильно подписать токен. JWT попросту не будет валидным.

Перейдем к следующей схеме на иллюстрации ниже. Схема похожа на аутентификацию с помощью сессий, но есть одно отличие. После введения кредов сервер дает юзеру не cookies с Session ID, а токен. Затем клиентское приложение добавляет токен к каждому запросу в виде специального заголовка Authorization.

При этом оно вписывает в него слово «Bearer» (предъявитель), а после пробела — сам токен. Также на схеме представлен Auth middleware. Это достаточно стандартная часть любого фреймворка, которая умеет проверять, в частности, валидность токена. То есть обработка на сервере проходит этап проверки.

Если токен не заэкспайрился и правильно подписан с помощью секретной строки, то пользователь аутентифицирован. В результате при использовании архитектуры микросервисов не нужно каждый запрос к ресурсным серверам прогонять через сервер аутентификации.

Logout и only one active device

Давайте подробнее рассмотрим задачи авторизации Logout и Only one active device. Сами по себе токены задуманы как stateless, то есть они не предназначены для размещения на сервере. А идея заключается в том, чтобы предусмотреть возможность хранения какой-то информации о них на сервере.

Такие токены будут храниться в «черном списке», пока не истечет срок их использования. С другой стороны, можно создать на сервере whitelist, что может оказаться даже несколько проще. Этот список станет реестром выданных токенов, с которым может сверяться система. Если предъявленный юзером токен не просрочен и указан в «белом списке», то он валиден.

Хранение самих токенов в базе данных сложно назвать безопасным. Можно ли как-то выйти из этой  ситуации? Да. Обратите внимание на иллюстрацию ниже: в разделе payload добавился пункт hash. Это некая рандомно сформированная строка. Она записывается в payload токена и в хранилище на сервере с привязкой к идентификатору пользователя.

Когда юзер предъявляет токен, система парсит его, проверяет срок действия токена и его подпись секретной строкой. Затем вытаскивает hash и сверяет его с пользовательским ID в хранилище. Это может быть key-value хранилище, основная база данных или другое хранилище, которое предоставит современный фреймворк.

Payload

Полезна нагрузка — это любые данные, которые вы хотите передать в токене. Стандарт предусматривает несколько зарезервированных полей:

  • iss — (issuer) издатель токена
  • sub — (subject) “тема”, назначение токена
  • aud — (audience) аудитория, получатели токена
  • exp — (expire time) срок действия токена
  • nbf — (not before) срок, до которого токен не действителен
  • iat — (issued at) время создания токена
  • jti — (JWT id) идентификатор токена

Все эти claims не являются обязательными, но их использование не по назначению может привести к коллизиям.

Любые другие данные можно передавать по договоренности между сторонами, использующими токен. Например, payload может выглядеть так:

Refresh tokens

В современных схемах аутентификации, основанных на JWT, после прохождения аутентификации пользователь получает два токена:

  • access token — JWT, на основе которого приложение идентифицирует и авторизует пользователя;
  • refresh token — токен произвольного формата, служащий для обновления access token.

Access token при таком подходе имеет сильно ограниченное время жизни (например, одну минуту). Refresh token же имеет длительное время жизни (день, неделя, месяц), но он одноразовый и служит исключительно для обновления access token пользователя.

Схема аутентификации в таком случае выглядит следующим образом:

  • пользователь проходит процедуру аутентификации и получает от сервера access token и refresh token;
  • при обращении к ресурсу пользователь передает в запросе свой access token, на основе которого сервер идентифицирует и авторизует клиента;
  • при истечении access token клиент передает в запросе свой refresh token и получает от сервера новые access token и refresh token;
  • при истечении refresh token пользователь заново проходит процедуру аутентификации.

Активация лицензии eset nod32

Чтобы активировать лицензию ESET откройте программу, далее Справка и поддержка » изменить лицензию.

Активируйте лицензию путём ввода сгенерированного лицензионного ключа в предыдущем пункте статьи.

Таким образом вы получили бесплатную лицензию антивируса ESET NOD32 на 1 месяц.

Лайфхак: каждый раз когда лицензия заканчивается, вы создаёте бесплатную временную почту, используя её создаёте учётную запись ESET и генерируете бесплатную лицензию. И так до бесконечности.

Аутентификация пользователей

В Django существует идея бекендов аутентификации. Не вдаваясь в подробности, бекенд – это, по сути, план принятия решения о том, аутентифицирован ли пользователь. Нам нужно создать собственный бекенд для поддержки JWT, поскольку по умолчанию он не поддерживается ни Django, ни Django REST Framework (DRF).

Создайте и откройте файл apps/authentication/backends.py и добавьте в него следующий код:

Верификация токенов

Мы всегда имеем возможность сохранить не только идентификатор пользователя (ID) с его токеном. Мы также можем хранить такие вещи, как дата истечения срока действия токена. В данном примере нам необходимо убедиться, что срок действия токена не прошел.

Вход пользователей в систему

Поскольку теперь пользователи могут зарегистрироваться в приложении, нам нужно реализовать для них способ входа в свою учетную запись. Далее, мы добавим сериализатор и представление, необходимые пользователям для входа в систему. Мы также начнем смотреть, как наш API должен обрабатывать ошибки.

LoginSerializer

Откройте файл apps/authentication/serializers.py и добавьте следующий импорт:

from django.contrib.auth import authenticate

После, наберите следующий код сериализатора в конце файла:

Генерация бесплатной лицензии eset nod32

Чтобы сгенерировать бесплатную лицензию ESET, сначала необходимо перейти в свой профиль:

Далее выбрать вкладку Лицензии » Генератор бесплатных лицензий » Выбрать версию ESET NOD32 и нажать кнопку Сгенерировать.

Для чего все это нужно?

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

Идея безопасного обмена токеном

Эта часть представляет собой концепцию. Мы собираемся сделать две вещи:

Изменение алгоритма подписи

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

Некоторые реализации библиотек для работы с JWT содержат логические ошибки, заключающиеся в том, что при получении токена, подписанного с использованием симметричного алгоритма (например, HS256), для проверки подписи в качестве ключевой фразы будет использован публичный ключ сервиса.

Для рассмотрения примера этого варианта атаки нам понадобится новый JWT:

Использование

Чтобы защитить наши API-методы, необходимо добавить атрибут

[AutoValidateAntiforgeryToken]

— для контроллера или

[ValidateAntiForgeryToken]

— для метода.

Использование алгоритма none

Как было упомянуто в первой части статьи, использование в заголовке JWT алгоритма none указывает на то, что токен не был подписан. В подобном токене отсутствует часть с подписью, и установить его подлинность становится невозможно.

Рассмотрим подобную атаку на нашем примере. Наш токен в незакодированном виде выглядит следующим образом

Ключевой момент:

В момент рефреша то есть обновления access token’a обновляются ОБА токена. Но как же refresh token может сам себя обновить, он ведь создается только после успешной аунтефикации ? refresh token в момент рефреша сравнивает себя с тем refresh token’ом который лежит в БД и вслучае успеха, а также если у него не истек срок, система рефрешит токены. Внимание при обновлении refresh token’a продливается также и его срок жизни.

Настройка asp.net core сервера

Middleware Services

ConfigureServices


services.AddAntiforgery(options => { options.HeaderName = "x-xsrf-token"; });
services.AddMvc();

Configure

app.UseAuthentication();
app.UseXsrfProtection(antiforgery);

Настройка cors-политики

Важно

: CORS-policy должна содержать

AllowCredentials()

. Это нужно, чтобы получить запрос с

Настройка jwt


В данном случае подойдет самая обычная реализация JWT из документации или любой статьи, с дополнительной настройкой

Настройка spa клиента

При расположении клиента и сервера на разных origin требуется дополнительная настройка и на клиенте. Необходимо оборачивать каждый запрос использованием

Пара слов о безопасности данных

Теперь, когда вы разобрались в устройстве и работе JSON веб-токенов, возникает естественный вопрос – защищены ли передаваемые данные?

Мы видели, что токен кодируется и подписывается, но – внимание! – не шифруется! (в чем разница между кодированием и шифрованием?) То есть эта обработка не обеспечивает безопасность данных. Нет никакой гарантии сохранности, поэтому не следует передавать в токенах что-то конфиденциальное.

Перегрузка exception_handler и non_field_errors_key

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

Начнем с создания project/exceptions.py, и добавления в него следующего кода:

from rest_framework.views import exception_handler


def core_exception_handler(exc, context):
    # Если возникает исключение, которые мы не обрабатываем здесь явно, мы
    # хотим передать его обработчику исключений по-умолчанию, предлагаемому
    # DRF. И все же, если мы обрабатываем такой тип исключения, нам нужен
    # доступ к сгенерированному DRF - получим его заранее здесь.
    response = exception_handler(exc, context)
    handlers = {
        'ValidationError': _handle_generic_error
    }
    # Определить тип текущего исключения. Мы воспользуемся этим сразу далее,
    # чтобы решить, делать ли это самостоятельно или отдать эту работу DRF.
    exception_class = exc.__class__.__name__

    if exception_class in handlers:
        # Если это исключение можно обработать - обработать :) В противном
        # случае, вернуть ответ сгенерированный стандартными средствами заранее
        return handlers[exception_class](exc, context, response)

    return response


def _handle_generic_error(exc, context, response):
    # Это самый простой обработчик исключений, который мы можем создать. Мы
    # берем ответ сгенерированный DRF и заключаем его в ключ 'errors'.
    response.data = {
        'errors': response.data
    }

    return response

Позаботившись об этом, откройте файл project/settings.py и добавьте новый параметр под названием REST_FRAMEWORK в конец файла:

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'project.exceptions.core_exception_handler',
    'NON_FIELD_ERRORS_KEY': 'error',
}

Так переопределяются стандартные настройки DFR. Чуть позже, мы добавим еще одну настройку, когда будем писать представления, требующие аутентификации пользователя. Попробуйте отправить еще один некорректный (с неверными почтой и/или паролем) запрос на вход с помощью Postman – сообщение об ошибке должно измениться.

Перехват токена

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

Во-первых, так как JWT передается в открытом виде, для получения хранящихся в части полезной нагрузки исходных данных достаточно применить к этой части функцию base64UrlDecode. То есть злоумышленник, перехвативший токен, сможет извлечь хранящиеся в токене данные о пользователе.

В соответствии с лучшими практиками для предотвращения подобной угрозы рекомендуется:

  • использовать при передаче токенов защищенное соединение;
  • не передавать в токенах чувствительные пользовательские данные, ограничившись обезличенными идентификаторами.

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

Здесь рекомендации будут следующие:

  • как и в первом случае, использовать защищенное соединение при передаче токенов;
  • ограничить время жизни JWT и использовать механизм refresh tokens.

Подбор ключа симметричного алгоритма подписи

При использовании симметричных алгоритмов для подписи JWT (HS256, HS512 и др.) злоумышленник может попытаться подобрать ключевую фразу.

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

В нашем примере из первой части статьи для подписи JWT в качестве ключевой фразы была использована строка password. Она простая, короткая и содержится во всех основных словарях для перебора паролей. Злоумышленнику не составит труда подобрать эту ключевую фразу с использованием программ John the Ripper или hashcat.

Рекомендации для защиты от атаки в этом случае такие:

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

Показать html форму регистрации

Когда вы нажмёте на меню Регистрация на навигационной панели, отобразится форма регистрации.

Преимущества jwt

Перечислим преимущества использования JWT в сравнении с классической схемой аутентификации, использующей сессии.

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

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

В-третьих, при использовании отдельного сервиса аутентификации становится возможным организовать единую точку входа в различные сервисы с одними и теми же учетными данными (SSO). Единожды пройдя процедуру аутентификации, пользователь сможет получить доступ со своим токеном к тем ресурсам, которые доверяют этому сервису аутентификации.

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

Благодаря перечисленным факторам схема аутентификации с использованием JWT широко используется в различных корпоративных приложениях. Особенно популярна эта схема в тех приложениях, которые реализуют парадигмы микросервисной архитектуры: при таком подходе каждый сервис получает необходимые ему сведения о пользователе непосредственно из токена, а не тратит время на получение этой информации из базы данных.

Пример имплементации:

Front-end:

Back-end:

Проблемы использования токенов

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

Но отсюда следует проблема: если выдать пользователю новую пару токенов, а старая пара еще валидна, то средств ее заэкспайрить нет.

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

Различные типовые задачи с использованием jwt

Сегодня в проектах приходится решать задачи, которые выходят за рамки описываемых токенов, но при этом они базируются на их использовании. Первая подобная задача — lockout-механизм. Он предполагает блокирование пользователя после нескольких безуспешных попыток аутентификации.

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

Более интересные задачи — Logout и Only one active device, которые в принципе связаны между собой.

Logout предполагает, что после нажатия кнопки логаута во фронтенд-приложении надо с подтверждением выхода еще и инвалидировать сами токены, сделать их недействительными.

Регистрация новых пользователей

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

RegistrationSerializer

Создайте файл apps/authentication/serializers.py и наберите туда следующий код:

Создание метода update()

Выполним запрос UPDATE, очистку данных и привязку.

Создание файла конфигурации (ядра)

Файл login.php не будет работать без файла core.php. Этот файл содержит общие настройки / переменные нашего приложения.

У нас есть переменные, используемые нашей библиотекой JWT для кодирования и декодирования токена. Значение $key должно быть вашим собственным и уникальным секретным ключом.

Вы также можете использовать exp – идентифицирует время истечения срока действия токена.

Откроем api/config/core.php и добавим следующий код.

Сообщить drf про наш аутентификационный бекенд

Мы должны явно указать Django REST Framework, какой бекенд аутентификации мы хотим использовать, аналогично тому, как мы сказали Django использовать нашу пользовательскую модель.

Откройте файл project/settings.py и обновите словарь REST_FRAMEWORK следующим новым ключом:

REST_FRAMEWORK = {
    ...
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'apps.authentication.backends.JWTAuthentication',
    ),
}

Сравнение сессий и jwt

Наверняка кто-то скажет: «Ура, вы переоткрыли механизм сессий», и в этом есть доля правды. Токен нужен для подтверждения аутентификации пользователя. А идентификатор сесии — это признак пользовательской сессии: находится ли сейчас юзер в системе, может ли он работать с ней, или сессия завершена.

В интернете вам может попасться такая немного саркастическая картинка:

Тест входа в систему

Введём следующий URL запрос:

В Body вставьте следующее значение JSON.

Нам нужно сохранить сгенерированный JWT чтобы потом его использовать для проверки.

Для проверки на неудачный вход в систему измените значение пароля на 222 (это неверный пароль).

Тест на успешный доступ

Введём следующий URL:

Введите JSON токен, который мы получили ранее. Приведенный ниже токен JSON отличается от вашего.

Так должно выглядеть в POSTMAN:

Чтобы проверить наличие неудачного доступа, просто добавьте слово «EDITED» в свой JWT. Это сделает JWT неправильным и приведет к отказу в доступе. Это должно выглядеть так.

Тест успешного обновления пользователя

Введём следующий URL в POSTMAN.

В разделе Body изменим информацию о текущем пользователе с помощью веб-токена JSON, который мы получили ранее (и сохранили).

Должно выглядеть примерно так.

При обновлении информации о пользователе происходит генерация нового токена JWT.

Чтобы проверить, не удалось ли обновить пользователя, вы можете просто добавить слово EDITED в правленную JWT, или просто удалить JWT. Это должно выглядеть следующим образом.

Уязвимости jwt

В этом разделе будут рассмотрены основные атаки на JWT и даны рекомендации по их предотвращению.

Формат jwt

JWT состоит из трех основных частей: заголовка (header), нагрузки (payload) и подписи (signature). Заголовок и нагрузка формируются отдельно в формате JSON, кодируются в base64, а затем на их основе вычисляется подпись. Закодированные части соединяются друг с другом, и на их основе вычисляется подпись, которая также становится частью токена.

Бывают и исключения, когда в JWT отсутствует подпись. Подобный случай будет рассмотрен далее.

Заголовок является служебной частью и состоит из двух полей: типа токена, в данном случае JWT, и алгоритма хэширования подписи:

{
  "typ": "JWT",
  "alg": "HS256"
}

Значение поля typ зачастую игнорируется приложениями, однако стандарт не рекомендует отказываться от него для обеспечения обратной совместимости.

Поле alg обязательно для заполнения. В приведенном случае был применен алгоритм HS256 (HMAC-SHA256), в котором для генерации и проверки подписи используется единый секретный ключ.

Для подписи JWT могут применяться и алгоритмы асимметричного шифрования, например RS256 (RSA-SHA256). Стандарт допускает использование и других алгоритмов, включая HS512, RS512, ES256, ES512, none и др.

Использование алгоритма none указывает на то, что токен не был подписан. В подобном токене отсутствует часть с подписью, и установить его подлинность невозможно.

Закодируем этот JSON в base64 и получим: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Шаг 3. подпись

Подпись токена вычисляется на основе его заголовка и полезной нагрузки по следующей схеме (псевдокод):

data = base64urlEncode(header)   "."   base64urlEncode(payload)
hashedData = hash(data, secret)
signature = base64urlEncode(hashedData)

Разберем по шагам:

// header
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

// payload
eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ
// signature
-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM

Шаг 4. cборка jwt

У нас уже есть все необходимые блоки для создания токена:

  • заголовок с указанием алгоритма хеширования;
  • полезная нагрузка с пользовательскими данными;
  • сгенерированная подпись.

Теперь можно собрать из них полноценный веб-токен по уже знакомой схеме:

header.payload.signature

Заголовок и полезная нагрузка предварительно кодируются в Base64URL, а подпись уже в правильном формате:

// JWT
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1nij9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhltq4zjitogzhyi1jzwyzota0njywymqifq.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM

Этот токен пользователь получит от сервера аутентификации и будет использовать при запросах к серверу приложения.

Шаг 5. верификация jwt

В демо-примере мы использовали хеширование с секретным ключом по алгоритму HS256. Изначально этот ключ известен только серверу аутентификации. Сервер приложения получает его при настройке процесса проверки подлинности.

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

Итоги

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

Создали четыре эндпоинта, которые позволяют пользователям регистрироваться, входить в систему, получать и обновлять информацию о своем аккаунте. На мой взгляд, это очень приятный фундамент, основа, на которой можно строить какой-то новый ламповый проект по своему усмотрению:) (однако же, есть куча сфер, о которых можно говорить часами, например на языке крутится следующий этап о том, чтобы завернуть это все в контейнеры докера, прикрутив постгрес, редис и селери).

Заключение

В этой статье рассмотрели один из примеров реализации REST приложения с Spring Security и JWT. Надеюсь данный вариант реализации кому то окажется полезным.

Полный код проекта выложен доступен на github

Похожее:  JWT: The Complete Guide to JSON Web Tokens

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *