Было время, когда единственным способом аутентифицировать себя с помощью приложения было предоставление своих учетных данных (обычно имя пользователя или адрес электронной почты и пароль), а затем использовался сеанс для сохранения состояния пользователя до выхода пользователя из системы. Чуть позже мы начали использовать API аутентификации. А в последнее время JWT или
В этой статье вы узнаете, что такое JWT и как использовать их с PHP для выполнения аутентифицированных пользовательских запросов.
JWT против сеансов
Но,
Данные хранятся в виде простого текста на сервере.
Несмотря на то, что данные обычно не хранятся в общей папке, любой, у кого есть достаточный доступ к серверу, может прочитать содержимое файлов сеанса.
Они включают запросы на чтение/запись файловой системы.
Каждый раз, когда начинается сеанс или его данные изменяются, серверу необходимо обновить файл сеанса. То же самое происходит каждый раз, когда приложение отправляет файл cookie сеанса. Если у вас большое количество пользователей, вы можете столкнуться с медленным сервером, если не используете альтернативные варианты хранения сеансов, такие как Memcached и Redis.
Распределенные/кластерные приложения.
Поскольку файлы сеансов по умолчанию хранятся в файловой системе, трудно иметь распределенную или кластерную инфраструктуру для приложений высокой доступности, которые требуют использования таких технологий, как балансировщики нагрузки и кластерные серверы. Необходимо внедрить другие носители данных и специальные конфигурации — и сделать это с полным осознанием их последствий.
JWT
Теперь давайте начнем изучать JWT. Спецификация JSON Web Token (RFC 7519) была впервые опубликована 28 декабря 2010 г. и последний раз обновлялась в мае 2015 г.
JWT имеют много преимуществ по сравнению с ключами API, в том числе:
Ключи API представляют собой случайные строки, тогда как JWT содержат информацию и метаданные. Эта информация и метаданные могут описывать широкий спектр вещей, таких как личность пользователя, данные авторизации и действительность токена в течение определенного периода времени или по отношению к домену.
JWT не требуют централизованного органа выдачи или отзыва.
JWT совместимы с OAUTH2.
Данные JWT можно проверить.
JWT имеют средства контроля истечения срока действия.
JWT предназначены для сред с ограниченным пространством, таких как заголовки авторизации HTTP.
Данные передаются в формате JavaScript Object Notation (JSON).
JWT представлены с использованием кодировки Base64url.
Как выглядит JWT?
Вот пример JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MTY5MjkxMDksImp0aSI6ImFhN2Y4ZDBhOTVjIiwic2NvcGVzIjpbInJlcG8iLCJwdWJsaWNfcmVwbyJdfQ.XCEwpBGvOLma4TCoh36FU7XhUbcskygS81HE1uHLf0E
На первый взгляд кажется, что строка представляет собой просто случайные группы символов, объединенных точкой или точкой. Таким образом, может показаться, что он не сильно отличается от ключа API. Однако, если вы присмотритесь, то увидите три отдельные строки.
Заголовок JWT
Первая строка — это заголовок JWT. Это строка JSON с кодировкой Base64 и
Симметричный алгоритм использует один ключ как для создания, так и для проверки токена. Ключ распределяется между создателем JWT и его потребителем. Очень важно убедиться, что только создатель и потребитель знают секрет. В противном случае любой может создать действительный токен.
Асимметричный алгоритм использует закрытый ключ для подписи токена и открытый ключ для его проверки. Эти алгоритмы следует использовать, когда общий секрет нецелесообразен или другим сторонам нужно только проверить целостность токена.
Полезная нагрузка JWT
Вторая строка — это полезная нагрузка JWT. Это также строка JSON в кодировке Base64,
Зарегистрированные претензии предопределены. Вы можете найти их список в 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
Форма входа
Пример формы входа с использованием HTML и JavaScript
Установив библиотеку, давайте пройдемся по коду входа в 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→getTimestamp (), // Issued at: time when the token was generated
'iss’ => $serverName, // Issuer
'nbf’ => $issuedAt→getTimestamp (), // Not before
'exp’ => $expire, // Expire
'userName’ => $username, // User name
];
Когда данные полезной нагрузки готовы к работе, мы используем статический метод
Метод:
преобразует массив в JSON
производить заголовки
подписывает полезную нагрузку
кодирует последнюю строку
Он принимает три параметра:
информация о полезной нагрузке
секретный ключ
алгоритм, используемый для подписи токена
При вызове echoрезультата функции возвращается сгенерированный токен:
<? php
// Encode the array to a JWT string.
echo JWT: encode (
$data,
$secretKey,
'HS512'
) ;
}
Использование JWT
Получение ресурса с помощью JavaScript и 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: {
'
},
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
При нажатии на кнопку «Получить текущую временную метку» делается
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:
Accept: */*
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0MjU1ODg4MjEsImp0aSI6IjU0ZjhjMjU1NWQyMjMiLCJpc3MiOiJzcC1qd3Qtc2ltcGxlLXRlY25vbTFrMy5jOS5pbyIsIm5iZiI6MTQyNTU4ODgyMSwiZXhwIjoxNDI1NTkyNDIxLCJkYXRhIjp7InVzZXJJZCI6IjEiLCJ1c2VyTmFtZSI6ImFkbWluIn19.
Предполагая, что 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 был извлечен, поэтому мы переходим к этапу декодирования и проверки. Для этого нам снова понадобится наш секретный ключ, который будет извлечен из среды или конфигурации приложения. Затем мы используем статический метод
Если он может быть успешно декодирован, мы пытаемся проверить его. Пример, который у меня есть, довольно упрощен, так как он использует только эмитента, а не временные метки до и после истечения срока действия. В реальном приложении вы, вероятно, также использовали бы ряд других утверждений.
$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;
}
Если токен недействителен, например,
Если процесс декодирования JWT не удался, это может быть так:
Количество предоставленных сегментов не соответствовало стандартным трем, как описано ранее.
Заголовок или полезные данные не являются допустимой строкой JSON.
Подпись недействительна, значит данные были подделаны!
Утверждение nbfустанавливается в JWT с отметкой времени, когда текущая отметка времени меньше этой.
Утверждение iatустанавливается в JWT с отметкой времени, когда текущая отметка времени меньше этой.
Утверждение expустанавливается в JWT с меткой времени, когда текущая метка времени больше этой.
Как видите, JWT имеет хороший набор элементов управления, которые помечают его как недействительный без необходимости вручную отзывать его или проверять его по списку действительных токенов.
Если процесс декодирования и проверки завершится успешно, пользователю будет разрешено сделать запрос, и ему будет отправлен соответствующий ответ.
В заключение
Это краткое введение в