Авторизация PHP с помощью JWT (веб-токены JSON)

Как выучить PHP3 Программирование и разработка

Как выучить PHP3

Было время, когда единственный способ аутентифицировать себя в приложении заключался в предоставлении ваших учетных данных (обычно имени пользователя или адреса электронной почты и пароля), а затем использовался сеанс для поддержания состояния пользователя до тех пор, пока пользователь не вышел из системы. Чуть позже мы начали использовать API аутентификации. А в последнее время JWT или веб-токены JSON все чаще используются как еще один способ аутентификации запросов к серверу.

В этой статье вы узнаете, что такое JWT и как использовать их с PHP для выполнения аутентифицированных запросов пользователей.

JWT против сеансов

Но сначала, почему сеансы — не такая уж хорошая вещь? Что ж, есть три основных причины:

  • Данные хранятся на сервере в виде обычного текста.
    Несмотря на то, что данные обычно не хранятся в общей папке, любой, у кого есть достаточный доступ к серверу, может прочитать содержимое файлов сеанса.
  • Они включают запросы на чтение / запись файловой системы.
    Каждый раз, когда начинается сеанс или его данные изменяются, серверу необходимо обновить файл сеанса. То же самое происходит каждый раз, когда приложение отправляет файл cookie сеанса. Если у вас большое количество пользователей, вы можете получить медленный сервер, если не используете альтернативные варианты хранения сеансов, такие как Memcached и Redis.
  • Распределенные / кластерные приложения.
    Поскольку файлы сеансов по умолчанию хранятся в файловой системе, трудно иметь распределенную или кластерную инфраструктуру для приложений высокой доступности — тех, которые требуют использования таких технологий, как балансировщики нагрузки и кластерные серверы. Необходимо реализовать другие носители данных и особые конфигурации — и делать это с полным осознанием их последствий.

JWT

Теперь давайте начнем изучать JWT. Спецификация веб-токена JSON (RFC 7519) была впервые опубликована 28 декабря 2010 г. и последний раз обновлялась в мае 2015 г.

У JWT есть много преимуществ перед ключами API, в том числе:

  • Ключи API — это случайные строки, тогда как JWT содержат информацию и метаданные. Эта информация и метаданные могут описывать широкий спектр вещей, таких как личность пользователя, данные авторизации и срок действия токена в пределах временного интервала или по отношению к домену.
  • Для JWT не требуется централизованный орган выдачи или отзыва.
  • JWT совместимы с OAUTH2.
  • Данные JWT можно проверить.
  • У JWT есть элементы управления сроком действия.
  • JWT предназначены для сред с ограниченным пространством, таких как заголовки авторизации HTTP.
  • Данные передаются в формате JavaScript Object Notation (JSON).
  • JWT представлены с использованием кодировки Base64url/
Читайте также:  Floor C++

Как выглядит JWT?

Вот пример JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MTY5MjkxMDksImp0aSI6ImFhN2Y4ZDBhOTVjIiwic2NvcGVzIjpbInJlcG8iLCJwdWJsaWNfcmVwbyJdfQ.XCEwpBGvOLma4TCoh36FU7XhUbcskygS81HE1uHLf0E

На первый взгляд кажется, что строка представляет собой просто случайные группы символов, объединенных точкой или точкой. Таким образом, он может не сильно отличаться от ключа API. Однако, если вы присмотритесь, есть три отдельные строки.

Заголовок JWT

Первая строка — это заголовок JWT. Это строка JSON в кодировке Base64 с кодировкой URL. Он указывает, какой криптографический алгоритм использовался для генерации подписи, и тип токена, который всегда имеет значение JWT. Алгоритм может быть, как симметричным, так и асимметричным.

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

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

Полезная нагрузка JWT

Вторая строка — это полезная нагрузка JWT. Это также строка JSON в кодировке Base64 с кодировкой URL. Он содержит несколько стандартных полей, которые называются «претензиями». Есть три типа требований: зарегистрированные, публичные и частные.

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

  • iat: отметка времени выпуска токена.
  • key: уникальная строка, которая может использоваться для проверки токена, но противоречит отсутствию централизованного органа эмитента.
  • iss: строка, содержащая имя или идентификатор эмитента. Может быть доменным именем и может использоваться для удаления токенов из других приложений.
  • nbf: отметка времени, когда токен должен считаться действительным. Должно быть равно или больше iat.
  • exp: отметка времени, когда токен должен перестать быть действительным. Должно быть больше iatи nbf.

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

Подпись JWT

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

Подпись JWT — это комбинация трех вещей:

  • заголовок JWT
  • полезная нагрузка JWT
  • секретное значение

Эти три подписаны цифровой подписью ( не зашифрованы ) с использованием алгоритма, указанного в заголовке JWT. Если мы расшифруем приведенный выше пример, у нас будут следующие строки JSON:

Заголовок JWT

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

Данные JWT

{
    "iat": 1416929109,
    "jti": "aa7f8d0a95c",
    "scopes": [
        "repo",
        "public_repo"
    ]
}

Попробуйте сами jwt.io, где вы можете поиграть с кодированием и декодированием ваших собственных JWT.

Давайте использовать JWT в приложении на основе PHP

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

Есть много способов подойти к интеграции JWT, но вот как мы это сделаем.

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

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

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

В упрощенном сценарии пользователь может запросить только один ресурс — файл PHP с подходящим названием resource.php. Он ничего не сделает, просто вернет строку, содержащую текущую временную метку на момент запроса.

Есть несколько способов использовать JWT при выполнении запросов. В нашем приложении JWT будет отправлен в заголовке авторизации Bearer.

Если вы не знакомы с авторизацией на предъявителя, это форма HTTP-аутентификации, при которой токен (например, JWT) отправляется в заголовке запроса. Сервер может проверить токен и определить, следует ли предоставить доступ «носителю» токена.

Вот пример заголовка:

Authorization: Bearer ab0dde18155a43ee83edba4a4542b973

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

Обратите внимание, что JWT не предназначен для замены файлов cookie сеанса.

Предпосылки

Для начала нам нужно, чтобы в наших системах были установлены PHP и Composer.

В корне проекта запустите composer install. Это приведет к включению Firebase PHP-JWT, сторонней библиотеки, которая упрощает работу с JWT, а также ламинаса-config, предназначенного для упрощения доступа к данным конфигурации в приложениях.

Форма входа

 

Установив библиотеку, давайте пройдемся по коду входа в authenticate.php. Сначала мы выполняем обычную настройку, гарантируя, что автозагрузчик, созданный Composer, доступен.

<?php

declare(strict_types=1);

use Firebase\JWT\JWT;

require_once('../vendor/autoload.php');

После получения формы, учетные данные проверяются в базе данных или другом хранилище данных. Для целей этого примера мы предполагаем, что они действительны, и устанавливаем $hasValidCredentialsзначение true.

<?php

// extract credentials from the request

if ($hasValidCredentials) {

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

Еще одна вещь, на которую стоит обратить внимание, это то, что $secretKeyона не будет инициализирована таким образом. Скорее всего, вы установите его в среде и извлекете с помощью библиотеки, такой как phpdotenv, или в файле конфигурации. Я избегал этого в этом примере, так как хочу сосредоточиться на коде JWT.

$secretKey  = 'bGS6lzFqvvSQ8ALbOxatm7/Vk7mLQyzqaS34Q4oR1ew=';
$issuedAt   = new DateTimeImmutable();
$expire     = $issuedAt->modify('+6 minutes')->getTimestamp();      // Add 60 seconds
$serverName = "your.domain.name";
$username   = "username";                                           // Retrieved from filtered POST data

$data = [
    'iat'  => $issuedAt,         // Issued at: time when the token was generated
    'iss'  => $serverName,       // Issuer
    'nbf'  => $issuedAt,         // Not before
    'exp'  => $expire,           // Expire
    'userName' => $username,     // User name
];

Когда данные полезной нагрузки готовы к работе, мы затем используем статический encodeметод php-jwt для создания JWT.

Метод:

  • преобразует массив в JSON
  • создать заголовки
  • подписывает полезную нагрузку
  • кодирует последнюю строку

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

  • информация о полезной нагрузке
  • секретный ключ
  • алгоритм, используемый для подписи токена

При вызове echoрезультата функции возвращается сгенерированный токен:

<?php
    // Encode the array to a JWT string.
    echo JWT::encode(
        $data,
        $secretKey,
        'HS512'
    );
}

Потребление JWT

Теперь, когда у клиента есть токен, вы можете сохранить его с помощью JavaScript или любого другого механизма, который вам больше нравится. Вот пример того, как это сделать с помощью ванильного JavaScript. В index.htmlпосле успешного представления формы, возвращаемая JWT хранятся в памяти, форма Логина скрыта, и отображается кнопка для запроса метки:

const store = {};
const loginButton = document.querySelector('#frmLogin');
const btnGetResource = document.querySelector('#btnGetResource');
const form = document.forms[0];

// Inserts the jwt to the store object
store.setJWT = function (data) {
  this.JWT = data;
};

loginButton.addEventListener('submit', async (e) => {
  e.preventDefault();

  const res = await fetch('/authenticate.php', {
    method: 'POST',
    headers: {
      'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
    },
    body: JSON.stringify({
      username: form.inputEmail.value,
      password: form.inputPassword.value
    })
  });

  if (res.status >= 200 && res.status <= 299) {
    const jwt = await res.text();
    store.setJWT(jwt);
    frmLogin.style.display = 'none';
    btnGetResource.style.display = 'block';
  } else {
    // Handle errors
    console.log(res.status, res.statusText);
  }
});

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

При нажатии кнопки «Получить текущую отметку времени» выполняется запрос GET resource.php, который устанавливает JWT, полученный после аутентификации, в заголовке авторизации.

btnGetResource.addEventListener('click', async (e) => {
  const res = await fetch('/resource.php', {
    headers: {
      'Authorization': `Bearer ${store.JWT}`
    }
  });
  const timeStamp = await res.text();
  console.log(timeStamp);
});

Когда мы нажимаем кнопку, делается запрос, подобный следующему:

GET /resource.php HTTP/1.1
Host: yourhost.com
Connection: keep-alive
Accept: */*
X-Requested-With: XMLHttpRequest
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0MjU1ODg4MjEsImp0aSI6IjU0ZjhjMjU1NWQyMjMiLCJpc3MiOiJzcC1qd3Qtc2ltcGxlLXRlY25vbTFrMy5jOS5pbyIsIm5iZiI6MTQyNTU4ODgyMSwiZXhwIjoxNDI1NTkyNDIxLCJkYXRhIjp7InVzZXJJZCI6IjEiLCJ1c2VyTmFtZSI6ImFkbWluIn19.HVYBe9xvPD8qt0wh7rXI8bmRJsQavJ8Qs29yfVbY-A0

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

Проверка JWT

Наконец, давайте посмотрим, как мы можем проверить токен в PHP. Как всегда, мы включили автозагрузчик Composer. Затем мы могли бы, при желании, проверить, был ли использован правильный метод запроса. Я пропустил код, чтобы продолжить работу с кодом, специфичным для JWT:

<?php
chdir(dirname(__DIR__));

require_once('../vendor/autoload.php');

// Do some checking for the request method here, if desired.

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

Регулярное выражение, которое я использовал здесь, будет пытаться извлечь токен из заголовка Bearer и сбросить все остальное. Если он не найден, возвращается неверный запрос HTTP 400:

if (! preg_match('/Bearer\s(\S+)/', $_SERVER['HTTP_AUTHORIZATION'], $matches)) {
    header('HTTP/1.0 400 Bad Request');
    echo 'Token not found in request';
    exit;
}

Обратите внимание, что по умолчанию, Apache не будет проходить в HTTP_AUTHORIZATIONзаголовок PHP.

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

Я полностью понимаю логику этого решения. Однако, чтобы избежать путаницы, добавьте в конфигурацию Apache следующее. Тогда код будет работать должным образом. Если вы используете NGINX, код должен работать должным образом:

RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.+)$
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

Затем мы пытаемся извлечь совпавший JWT, который будет во втором элементе $matchesпеременной. Если он недоступен, значит, JWT не был извлечен, и возвращается неверный запрос HTTP 400:

$jwt = $matches[1];
if (! $jwt) {
    // No token was able to be extracted from the authorization header
    header('HTTP/1.0 400 Bad Request');
    exit;
}

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

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

$secretKey  = 'bGS6lzFqvvSQ8ALbOxatm7/Vk7mLQyzqaS34Q4oR1ew=';
$token = JWT::decode($jwt, $secretKey, ['HS512']);
$now = new DateTimeImmutable();
$serverName = "your.domain.name";

if ($token->iss !== $serverName ||
    $token->nbf > $now->getTimestamp() ||
    $token->exp < $now->getTimestamp())
{
    header('HTTP/1.1 401 Unauthorized');
    exit;
}

Если токен недействителен, потому что, например, срок действия токена истек, пользователю будет отправлен заголовок HTTP 401 Unauthorized, и сценарий завершится.

Если процесс декодирования JWT завершился неудачно, это могло быть так:

  • Количество предоставленных сегментов не соответствовало трем стандартным, как описано ранее.
  • Заголовок или полезная нагрузка не являются допустимой строкой JSON.
  • Подпись недействительна, значит, данные были подделаны!
  • nbfТребование устанавливается в JWT с меткой времени, когда ток метка времени меньше, чем это.
  • iatТребование устанавливается в JWT с меткой времени, когда ток метка времени меньше, чем это.
  • expТребование устанавливается в JWT с меткой времени, когда текущая метка времени больше, чем это.

Если процесс декодирования и проверки завершится успешно, пользователю будет разрешено сделать запрос, и ему будет отправлен соответствующий ответ.

Заключение

Это краткое введение в веб-токены JSON или JWT и способы их использования в приложениях на основе PHP. С этого момента вы можете попробовать реализовать JWT в своем следующем API, возможно, попробовав некоторые другие алгоритмы подписи, которые используют асимметричные ключи, такие как RS256, или интегрируя его в существующий сервер аутентификации OAUTH2 в качестве ключа API.

 

Оцените статью
bestprogrammer.ru
Добавить комментарий