PHP авторизация с помощью JWT (JSON Web Tokens)

  1. Против сессий
  2. JWT
  3. Давай поиграем
  4. Заключение

Если вам нравятся темы по компьютерной безопасности, вы будете знать, что одной из самых обсуждаемых и противоречивых тем является аутентификация пользователя. В его контексте вы найдете широкий спектр областей исследования, от новых механизмов до юзабилити. Таким образом, к моему удивлению, JSON Web Tokens - тема не часто говорили о и я думаю, что это заслуживает того, чтобы быть в центре внимания сегодня. Мы увидим, как легко интегрировать его в механизм аутентификации API.

говорили о

Против сессий

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

Некоторые из недостатков выдачи учетных данных приложению и поддержания состояния пользователя по отношению к приложению с использованием сеансовых файлов cookie:

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

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

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

При работе с сервисными API, которые имеют ограниченные сервисные вызовы, вам нужно будет добавлять свой ключ к каждому выполненному запросу (либо в заголовке запроса, таком как «Авторизация», либо в строке запроса URL). Ключи API обычно основаны на централизованном механизме управления ими. Поэтому, если вы хотите пометить ключ API как недействительный, его следует отозвать на стороне приложения.

JWT

С октября 2010 года было выдвинуто несколько предложений по использованию токенов на основе JSON . JWT или JSON Web Token были предложены в декабре 2010 года и имели следующие характеристики:

  • Предназначено для сред с ограниченным пространством, таких как заголовки авторизации HTTP или параметры строки запроса.
  • Данные для передачи в формате нотации объектов Javascript (JSON)
  • Данные должны быть полезными данными JSON Web Signature (JWS)
  • Представлено с использованием кодировки URL Base64

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

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

  • Ключи API - это просто случайные строки, в то время как JWT содержат информацию и метаданные, которые могут описывать личность пользователя, данные авторизации и действительность токена в пределах временного интервала или домена.
  • JWT не требуют централизованной выдачи или отзыва полномочий.
  • OAUTH2 совместимый.
  • Данные JWT могут быть проверены.
  • У JWT есть контроль истечения.

19 мая 2015 года JWT стал опубликованным IETF RFC 7519 ,

JWT будет выглядеть следующим образом:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MTY5MjkxMDksImp0aSI6ImFhN2Y4ZDBhOTVjIiwic2NvcGVzIjpbInJlcG8iLCJwdWJsaWNfcmVwbyJdfQ.XCEwpBGvOLma4TCoh36FU7XhUbcskygS81HE1uHLf0E

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

Первая и вторая строки Base64 URL-кодированные строки JSON, поэтому, если мы их декодируем, мы получим следующие результаты:

{"alg": "HS256", "typ": "JWT"} {"iat": 1416929109, "jti": "aa7f8d0a95c", "scopes": ["repo", "public_repo"]}

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

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

Вы можете взглянуть на jwt.io где вы можете поиграть с кодированием и декодированием JWT.

Давай поиграем

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

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

Для начала мы устанавливаем PHP-JWT с композитором требуется firebase / php-jwt. в пример приложения Разработанный для этого урока, я также использую zend-config и zend-http, так что, если вы хотите следовать, не стесняйтесь устанавливать и их:

композитор требует firebase / php-jwt: dev-master композитор требует zendframework / zend-config: ~ 2.3 композитор требует zendframework / zend-http: ~ 2.3

Есть еще одна библиотека PHP, хосе от namshi если вы хотите поиграть с ним позже.

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

<? php require_once ('vendor / autoload.php'); / * * Настройки приложения, подключение к базе данных, очистка данных и пользовательские процедуры * здесь. * / $ config = Factory :: fromFile ('config / config.php', true); // Создаем объект Zend Config if ($ credentialsAreValid) {$ tokenId = base64_encode (mcrypt_create_iv (32)); $ выпустили = время (); $ notBefore = $ выпустил + 10; // Добавление 10 секунд $ expire = $ notBefore + 60; // Добавление 60 секунд $ serverName = $ config-> get ('serverName'); // Получить имя сервера из конфигурационного файла / * * Создать токен в виде массива * / $ data = ['iat' => $ assignAt, // Выдано в момент, когда токен был сгенерирован 'jti' => $ tokenId , // Json Token Id: уникальный идентификатор для токена 'iss' => $ serverName, // Issuer 'nbf' => $ notBefore, // Не раньше, чем 'exp' => $ expire, // Expire 'data' => [// Данные, относящиеся к подписавшему пользователю 'userId' => $ rs ['id'], // идентификатор пользователя из таблицы пользователей 'userName' => $ username, // Имя пользователя]]; / * * Больше кода здесь ... * /}

Обратите внимание, что вы можете определить структуру данных так, как хотите, однако есть некоторые зарезервированные утверждения, такие как те, которые использовались выше:

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

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

Преобразовать этот массив в JWT очень просто:

<? php / * * Код здесь ... * / / * * Извлеките ключ, который приходит из файла конфигурации. * * Лучшее предложение - это ключ для двоичной строки и * сохранения ее в кодированном виде в файле конфигурации. * * Может быть сгенерировано с помощью base64_encode (openssl_random_pseudo_bytes (64)); * * держите это в безопасности! Вам понадобится точный ключ для проверки * токена позже. * / $ secretKey = base64_decode ($ config-> get ('jwtKey')); / * * Кодировать массив в строку JWT. * Второй параметр - это ключ для кодирования токена. * * Выходная строка может быть проверена по адресу http://jwt.io/ * / $ jwt = JWT :: encode ($ data, // Данные для кодирования в JWT $ secretKey, // Ключ подписи 'HS512' // Алгоритм, используемый для подписи токена, см. Https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3); $ unencodedArray = ['jwt' => $ jwt]; echo json_encode ($ unencodedArray);

JWT :: encode () позаботится обо всем (преобразование массива в JSON, создание заголовков, подписание полезной нагрузки и кодирование окончательной строки). Вы захотите сделать свой секретный ключ длинной двоичной строкой, закодировать его в файле конфигурации и никогда не раскрывать его. Иметь это прямо в вашем коде - плохая идея.

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

$ (function () {var store = store || {}; / * * Устанавливает jwt для объекта store * / store.setJWT = function (data) {this.JWT = data;} / * * Отправить форму входа через ajax * / $ ("# frmLogin"). submit (function (e) {e.preventDefault (); $ .post ('auth / token', $ ("# frmLogin"). serialize (), function (data) ) {store.setJWT (data.JWT);}). fail (function () {alert ('error');});});});

Теперь давайте восстановим ресурс, который защищен нашим механизмом JWT.

При нажатии на кнопку «Получить ресурс >>», если все в порядке, вы должны увидеть изображение в серой области. Давайте используем ajax-вызов для отправки запроса в службу ресурсов:

$ ("# btnGetResource"). click (function (e) {e.preventDefault (); $ .ajax ({url: 'resource / image', beforeSend: function (request) {request.setRequestHeader ('Authorization', '' Носитель '+ store.JWT);}, тип:' GET ', success: function (data) {// Красиво декодировать и отображать возвращаемые данные.}, Error: function () {alert (' error ');}} );});

Пожалуйста, обратите внимание на параметр beforeSend. Мы говорим jQuery, что перед каждым запросом через этот вызов нам нужно установить заголовок Authorization с содержимым JWT в формате Bearer [JWT]. Поэтому, когда мы нажимаем кнопку, делается следующий запрос:

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

Теперь мы можем увидеть, что такое защищенный ресурс:

Вот как мы проверяем токен в службе ресурсов.

<? php chdir (dirname (__ DIR__)); require_once ( 'поставщик / autoload.php'); использовать Zend \ Config \ Config; использовать Zend \ Config \ Factory; использовать Zend \ Http \ PhpEnvironment \ Request; / * * Получить все заголовки из HTTP-запроса * / $ request = new Request (); if ($ request-> isGet ()) {$ authHeader = $ request-> getHeader ('authorization'); / * * Найдите заголовок 'authorization' * / if ($ authHeader) {/ * * Извлеките jwt из носителя * / list ($ jwt) = sscanf ($ authHeader-> toString (), 'Authorization: Bearer% с); if ($ jwt) {try {$ config = Factory :: fromFile ('config / config.php', true); / * * декодировать jwt, используя ключ из config * / $ secretKey = base64_decode ($ config-> get ('jwtKey')); $ token = JWT :: decode ($ jwt, $ secretKey, array ('HS512')); $ asset = base64_encode (file_get_contents ('http://lorempixel.com/200/300/cats/')); / * * вернуть защищенный ресурс * / header ('Content-type: application / json'); echo json_encode (['img' => $ asset]); } catch (Exception $ e) {/ * * токен не может быть декодирован. * это вероятно потому, что подпись не может быть проверена (подделанный токен) * / header ('HTTP / 1.0 401 Unauthorized'); }} else {/ * * Не удалось извлечь токен из заголовка авторизации * / header ('HTTP / 1.0 400 Bad Request'); }} else {/ * * В запросе отсутствует токен авторизации * / header ('HTTP / 1.0 400 Bad Request'); echo 'токен не найден в запросе'; }} else {header ('HTTP / 1.0 405 Метод не разрешен'); }

Я использую Zend \ Http \ PhpEnvironment \ Request, чтобы немного упростить работу с извлечением типов HTTP-запросов и заголовков:

$ request = new Request (); if ($ request-> isGet ()) {// Будет обрабатывать только HTTP-запросы GET. $ authHeader = $ request-> getHeader ('авторизация'); // ...

Теперь давайте выясним, есть ли в заголовке авторизации строка JWT:

/ * * Найдите заголовок «authorization» * / if ($ authHeader) {/ * * Извлеките JWT из носителя * / list ($ jwt) = sscanf ($ authHeader-> toString (), 'Authorization: Bearer% с); // БОЛЬШЕ КОДА}

Таким образом, переменная $ jwt будет содержать содержимое потенциального JWT.

Если вы не хотите иметь дело с заголовками HTTP-авторизации, вы можете выбрать одну из альтернатив: включить токен в запрос в качестве параметра URL:

GET /resource.php?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0MjU1ODg4MjEsImp0aSI6IjU0ZjhjMjU1NWQyMjMiLCJpc3MiOiJzcC1qd3Qtc2ltcGxlLXRlY25vbTFrMy5jOS5pbyIsIm5iZiI6MTQyNTU4ODgyMSwiZXhwIjoxNDI1NTkyNDIxLCJkYXRhIjp7InVzZXJJZCI6IjEiLCJ1c2VyTmFtZSI6ImFkbWluIn19.HVYBe9xvPD8qt0wh7rXI8bmRJsQavJ8Qs29yfVbY-A0 HTTP / 1.1 Host: yourhost.com Connection: Keep-жив Accept: * / * X-Requested-With: XMLHttpRequest

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

$ secretKey = base64_decode ($ config-> get ('jwtKey')); / * * декодировать JWT, используя ключ из config * / $ token = JWT :: decode ($ jwt, $ secretKey, array ('HS512'));

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

  1. Количество предоставленных сегментов не соответствует стандарту 3, как описано ранее.
  2. Заголовок или полезная нагрузка не являются допустимой строкой JSON
  3. Подпись недействительна, что означает, что данные были подделаны!
  4. Заявление nbf устанавливается в JWT с отметкой времени, когда текущая отметка времени меньше этой.
  5. Заявка iat устанавливается в JWT с отметкой времени, когда текущая отметка времени меньше этой.
  6. Утверждение exp устанавливается в JWT с отметкой времени, когда текущая отметка времени больше этой.

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

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

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

Наконец, мы запрашиваем изображение от lorempixel.com , base64 закодировать его и вернуть в виде строки ответа json:

$ asset = base64_encode (file_get_contents ('http://lorempixel.com/200/300/cats/')); / * * вернуть защищенный ресурс * / header ('Content-type: application / json'); echo json_encode (['img' => $ asset]);

Если вы хотите поиграть с примером приложения, вы можете проверить мой репо проекта для этой статьи следуйте инструкциям README и внимательно посмотрите на код.

Если вы хотите поиграть с примером приложения, вы можете проверить мой   репо проекта   для этой статьи следуйте инструкциям README и внимательно посмотрите на код

Заключение

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

Lt;?
Php?
Помните секретный ключ, который мы использовали ранее для генерации токена?