DevGang
Авторизоваться

Как реализовать веб-токены JSON (JWT) в PHP — Учебное пособие по аутентификации PHP

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

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

Что такое веб-токены JSON (JWT)?

Веб-токен JSON — это строка, состоящая из трех частей, каждая из которых соединена точкой (.), а затем закодирована в формате Base64url.

Вот три части JWT:

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

 $header = json_encode([
            "alg" => "HS256",
            "typ" => "JWT"
        ]);

        $header = $this->base64urlEncode($header);

Полезная нагрузка: в структуре JWT полезная нагрузка инкапсулирует определенные индексы, называемые утверждениями, содержащие пользовательские данные, закодированные с использованием base64url.

$payload = [
    "id" => $user['id'],
    "name" => $user["name"]
];

  $payload = $this->base64urlEncode($payload);

Генерируется путем создания хеша заголовка и полезной нагрузки в сочетании с секретным ключом, который обычно генерируется в виде 256 бит или 32 байта. По соглашению секретный ключ соответствует размеру выходного хэша.

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

 $signature = hash_hmac("sha256", $header . "." . $payload, $secret_key, true);
 $signature = $this->base64urlEncode($signature);

Веб-токен JSON — это просто комбинация заголовка, полезных данных и подписи, где каждый компонент объединяется с точками (“.”) между ними:

$header . "." . $payload . "." . $signature;

Начнём!

Чтобы запустить этот проект, загрузите стартовую версию проекта по следующей ссылке: Аутентификация PHP с помощью учебника JWT.

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

Чтобы начать настройку проекта, используйте Git, выполнив следующую команду:

git clone https://github.com/Oghenekparobo/php_auth_jwt_tut.git

Эта команда клонирует репозиторий проекта в вашу локальную систему, что позволит вам продолжить процесс установки и настройки.

После клонирования проекта из GitHub структура вашего проекта должна соответствовать следующему макету:

структура проекта на VS Code
структура проекта на VS Code
project_root/
│
├── api/
│ ├── .htaccess
│ ├── index.php
│ └── (other PHP files)
│
├── vendor/
│ ├── (Composer dependencies)
│ └── ...
│
├── .env
├── README.md
└── (other project files)

В этой структуре:

  • Каталог api/ содержит файлы PHP, отвечающие за обработку запросов и ответов API. Он включает в себя файл .htaccess для перезаписи URL-адресов и файл index.php, а также другие файлы PHP для определенных функций.
  • Каталог vendor/ содержит зависимости Composer, установленные для проекта. Эти зависимости управляются Composer и не должны изменяться напрямую.
  • Файл .env содержит переменные среды, необходимые для настройки среды приложения, такие как учетные данные базы данных и ключи API.
  • Файл README.md содержит важную информацию о проекте, включая инструкции по установке, рекомендации по использованию и любую другую соответствующую информацию.

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

Отправка наших запросов

При ссылке на наш проект URL-адрес обычно имеет следующую структуру: http://localhost/php_auth_jwt_tut/api. Однако в зависимости от названия проекта этот URL-адрес может соответственно различаться. Тем не менее, базовый URL-адрес остается неизменным: http://localhost/php_auth_jwt_tut/api.

Благодаря нашей реализации перезаписи URL-адресов в файле .htaccess дополнительные префиксы, такие как index.php или .php, не нужны при доступе к нашим URL-адресам. Мы тщательно настроили параметры нашего сервера, чтобы обеспечить плавную навигацию без этих префиксов.

Проще говоря, доступ к конечным точкам API нашего проекта можно осуществить непосредственно из базового URL: http://localhost/php_auth_jwt_tut/api. Этот оптимизированный подход улучшает взаимодействие с пользователем и устраняет ненужную сложность этого проекта.

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

Во-первых, мы просто распечатаем любой желаемый результат из файла index.php. Это помогает нам убедиться, что наша конечная точка работает правильно. Затем мы проверим конечную точку, получив доступ к URL-адресу: http://localhost/php_auth_jwt_tut/api.

Кроме того, мы настроим подключение к базе данных, чтобы обеспечить бесперебойную связь между конечными точками нашего API и базой данных. Для удобства все необходимые конфигурации включены в файл bootstrap.php. Импортировав этот файл в наш index.php, управление конфигурациями и импортом становится проще.

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

Тестирование подключения к базе данных

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

В папке src нашего проекта мы находим основные классы и шлюзы, включая необходимые подключения к базе данных для нашего проекта. За это отвечает файл data.php, уже настроенный для установления соединения с базой данных PDO.

Примечание. Перед использованием обязательно отрегулируйте или настройте переменные в файле среды .env в соответствии с учетными данными вашей базы данных.

.env-файл:

DB_HOST= 'db_host'
DB_NAME = 'db_name'
DB_USER = 'db_user'
DB_PASS = 'db_password'
SECRET_KEY = "secret_key"

bootstrap.php:

require dirname(__DIR__)  . '/vendor/autoload.php';

set_error_handler('ErrorHandler::handleError');
set_exception_handler('ErrorHandler::handleException');

$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
$dotenv->load();

header("Content-type: application/json; charset=UTF-8");


$database = new Database(
    $_ENV["DB_HOST"],
    $_ENV["DB_NAME"],
    $_ENV["DB_USER"],
    $_ENV["DB_PASS"]
);

Код подготавливает наше приложение, выполняя несколько ключевых действий. Во-первых, он загружает необходимые файлы для плавной обработки ошибок и исключений. Затем он извлекает переменные среды из специального файла (.env), где мы храним важные настройки, такие как информация базы данных. После этого он сообщает приложению отправить обратно данные в определенном формате (JSON). Наконец, он устанавливает соединение с базой данных, используя информацию из файла .env. Такая настройка гарантирует бесперебойную и безопасную работу нашего приложения.

database.php:

class Database
{
    private ?PDO $conn = null;

    public function __construct(
        private string $host,
        private string $name,
        private string $user,
        private string $password
    ) {
    }

    public function getConnection(): ?PDO
    {
        try {
            if ($this->conn === null) {
                $this->conn = new PDO("mysql:host=$this->host;dbname={$this->name}", $this->user, $this->password);
                $this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                $this->conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
                $this->conn->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false);
            }

            return $this->conn;
        } catch (PDOException $e) {
            echo "Connection failed: " . $e->getMessage();
            return null;
        }
    }
}

Включите оператор echo, чтобы указать успешное соединение с базой данных, прежде чем возвращать объект $this->conn. Это помогает проверить состояние соединения и обеспечить бесперебойную работу приложения. Например:

try {
    if ($this->conn === null) {
        $this->conn = new PDO("mysql:host=$this->host;dbname={$this->name}", $this->user, $this->password);
        $this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
        $this->conn->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false);
        
        // Echo message indicating successful database connection
        echo "Database connected successfully.";
    }
    
    return $this->conn;
} catch (PDOException $e) {
    echo "Connection failed: " . $e->getMessage();
    return null;
}

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

Вызов функции getConnection в файле index.php должен выглядеть следующим образом:

index.php файл
index.php файл

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

база данных успешно подключена
база данных успешно подключена
Примечание: Обязательно очистите эхо после того, как мы закончим.

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

Для эффективного управления пользовательскими данными мы реализуем минимальное интерфейсное решение, создав новый файл с именем Register.php в корневом каталоге нашего проекта. Кроме того, мы представим таблицу стилей style.css для улучшения визуального представления страницы регистрации.

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

register.php

require __DIR__ . "/vendor/autoload.php";

if ($_SERVER["REQUEST_METHOD"] === "POST") {

    $dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
    $dotenv->load();

    $database = new Database(
        $_ENV["DB_HOST"],
        $_ENV["DB_NAME"],
        $_ENV["DB_USER"],
        $_ENV["DB_PASS"]
    );

    $conn = $database->getConnection();

    $sql = "INSERT INTO user (name, username, password_hash)
            VALUES (:name, :username, :password_hash)";

    $stmt = $conn->prepare($sql);

    $password_hash = password_hash($_POST["password"], PASSWORD_DEFAULT);


    $stmt->bindValue(":name", $_POST["name"], PDO::PARAM_STR);
    $stmt->bindValue(":username", $_POST["username"], PDO::PARAM_STR);
    $stmt->bindValue(":password_hash", $password_hash, PDO::PARAM_STR);


    $stmt->execute();

    echo "Thank you for registering.";
    exit;
}

?>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>User Registration</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <div class="container">
        <h2>User Registration</h2>
        <form action="register.php" method="post">
            <div class="form-group">
                <label for="name">Name:</label>
                <input type="text" id="name" name="name" required>
            </div>
            <div class="form-group">
                <label for="username">Username:</label>
                <input type="text" id="username" name="username" required>
            </div>
            <div class="form-group">
                <label for="password">Password:</label>
                <input type="password" id="password" name="password" required>
            </div>
            <div class="form-group">
                <input type="submit" value="Register">
            </div>
        </form>
    </div>
</body>

</html>

style.css

body,
html {
  margin: 0;
  padding: 0;
  font-family: Arial, sans-serif;
}

.container {
  background-color: #fff;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
  width: 300px;
  margin: 0 auto;
}

h2 {
  text-align: center;
  margin-bottom: 20px;
}

.form-group {
  margin-bottom: 20px;
}

label {
  font-weight: bold;
}

input[type="text"],
input[type="password"] {
  width: 100%;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
}

input[type="submit"] {
  width: 100%;
  padding: 10px;
  border: none;
  background-color: #007bff;
  color: #fff;
  border-radius: 4px;
  cursor: pointer;
}

input[type="submit"]:hover {
  background-color: #0056b3;
}
Примечание: Убедитесь, что эти файлы расположены в корневом каталоге вашего проекта. Пользовательский интерфейс будет выглядеть следующим образом:

Процесс регистрации

Успешная регистрация

страница успешной регистрации
страница успешной регистрации

Структура нашей базы данных показана на изображении ниже:

структура базы данных в phpMyAdmin
структура базы данных в phpMyAdmin

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

Настройка класса JWT

В шаблоне нашего проекта файл Jwt.php уже создан. Теперь давайте приступим к созданию нашего класса Jwt и реализуем логику кодирования и генерации токена JWT.

Кодирование в JWT

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

Скопируйте следующий фрагмент кода и вставьте его в класс Jwt, расположенный в папке src.

class Jwt
{

    public function __construct(private string $key)
    {

    }

  
    private function base64URLEncode(string $text): string
    {

        return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($text));
    }

  
}

Этот класс отвечает за кодирование токенов JWT с использованием схемы кодирования base64url. Он включает в себя конструктор, который принимает ключевой параметр, который предположительно представляет собой секретный ключ, используемый для кодирования токенов. Кроме того, он содержит закрытый метод с именем base64URLEncode, который выполняет операцию кодирования base64URL.

Метод base64URLEncode: метод base64urlEncode — это частная функция в классе Jwt. Он принимает строковый параметр, текст, и возвращает версию входной строки в кодировке Base64URL. Метод сначала применяет стандартную кодировку base64 к входному тексту с помощью функции base64_encode.

Затем он заменяет символы '+' (плюс), '/' (косая черта) и '=' (знак равенства) на '-' (дефис), '_' (подчеркивание) и пустую строку соответственно. Эта замена необходима для обеспечения совместимости со схемой кодирования base64URL, в которой используются символы, безопасные для URL. Наконец, метод возвращает строку в кодировке base64URL.

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

Метод кодирования

Вставьте следующий фрагмент кода в свой класс Jwt:

public function encode(array $payload): string
    {

        $header = json_encode([
            "alg" => "HS256",
            "typ" => "JWT"
        ]);

        $header = $this->base64URLEncode($header);
        $payload = json_encode($payload);
        $payload = $this->base64URLEncode($payload);

        $signature = hash_hmac("sha256", $header . "." . $payload, $this->key, true);
        $signature = $this->base64URLEncode($signature);
        return $header . "." . $payload . "." . $signature;
    }

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

Метод encode — это то место, где происходит волшебство. Он берет некоторые данные (которые мы называем полезной нагрузкой) и превращает их в токен JWT. Вот как это работает:

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

Метод base64URLEncode гарантирует, что данные закодированы в формате, подходящем для URL-адресов, что делает их безопасными для передачи через Интернет. В нашем классе Jwt этот метод используется внутри функции encode для кодирования разделов заголовка и полезных данных токена.

С учетом вышесказанного, давайте приступим к тестированию и посмотрим на наш первый токен JWT, ура!

Конечная точка входа в систему

В нашем проекте папка api служит точкой входа для запросов. Теперь, когда мы реализовали алгоритм кодирования и создания токена JWT, давайте приступим к его тестированию. Для этого мы создадим файл login.php в нашей папке API. В этом файле мы отправим запрос, содержащий имя пользователя и пароль пользователя, профилированного или созданного в нашем пользовательском интерфейсе Register.php. Мы передадим необходимые данные пользователя в формате JSON:

{
    "username": "test",
    "password": "12345"
}

Пожалуйста, интегрируйте следующий фрагмент кода в файл login.php, расположенный в нашем каталоге API:


require __DIR__ . '/bootstrap.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    header('ALLOW: POST');
    exit();
}

$contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';


if ($contentType !== 'application/json') {
    http_response_code(415);
    echo json_encode(["message" => "Only JSON content is supported"]);
    exit();
}

$data = json_decode(file_get_contents('php://input'), true);

if ($data === null) {
    http_response_code(400);
    echo json_encode(["message" => "Invalid JSON data"]);
    exit();
}


if (!array_key_exists('username', $data) || !array_key_exists('password', $data)) {
    http_response_code(400);
    echo json_encode(["message" => "Missing login credentials"]);
    exit();
}

Этот фрагмент кода служит внутренней логикой для обработки запросов на вход пользователей. Он начинается с включения файла bootstrap.php для инициализации основных компонентов. Затем он проверяет, является ли метод входящего запроса POST, и в противном случае возвращает ошибку «Метод не разрешен».

Затем он проверяет, что тип контента запроса — JSON, и в противном случае отвечает ошибкой «Неподдерживаемый тип носителя». Код приступает к декодированию данных JSON из тела запроса и проверяет их достоверность. Если данные JSON недействительны или в них отсутствуют ключи «имя пользователя» и «пароль», возвращается ошибка «Неверный запрос».

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


class UserGateway
{

    private PDO $conn;

    public function __construct(Database $database)
    {
        $this->conn = $database->getConnection();
    }


    public function getByUsername(string $username): array | false
    {
        $sql = 'SELECT * FROM user WHERE username = :username';
        $stmt = $this->conn->prepare($sql);
        $stmt->bindValue(':username', $username, PDO::PARAM_STR);

        $stmt->execute();

        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
}

По ходу дела вы увидите необходимость этого.

Перейдите к файлу login.php.

Хорошо, после небольшой рекламной паузы мы можем создать экземпляр класса с именем $user_gateway и передать ему наше соединение $database. Затем мы извлекаем данные пользователя на основе предоставленного имени пользователя из экземпляра $user_gateway с помощью метода getByUsername(). Если пользователь не найден (что указывает на неверную аутентификацию), мы возвращаем статус ответа HTTP 401 вместе с соответствующим сообщением об ошибке в формате JSON.

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

Код для реализации этой логики будет следующим

$user_gateway = new UserGateway($database);

$user = $user_gateway->getByUsername($data['username']);

if ($user === false) {
    http_response_code(401);
    echo json_encode(["message" => "invalid authentication"]);
    exit;
}

if (!password_verify($data['password'], $user['password_hash'])) {
    http_response_code(401);
    echo json_encode(["message" => "invalid authentication"]);
    exit;
}


$payload = [
    "id" => $user['id'],
    "name" => $user["name"]
];


$JwtController = new Jwt($_ENV["SECRET_KEY"]);

$token =$$JwtController->encode($payload);

echo json_encode(["token" => $token]);

Полный код login.php

<?php

require __DIR__ . '/bootstrap.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    header('ALLOW: POST');
    exit();
}

$contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';


if ($contentType !== 'application/json') {
    http_response_code(415);
    echo json_encode(["message" => "Only JSON content is supported"]);
    exit();
}

$data = json_decode(file_get_contents('php://input'), true);

if ($data === null) {
    http_response_code(400);
    echo json_encode(["message" => "Invalid JSON data"]);
    exit();
}


if (!array_key_exists('username', $data) || !array_key_exists('password', $data)) {
    http_response_code(400);
    echo json_encode(["message" => "Missing login credentials"]);
    exit();
}

$user_gateway = new UserGateway($database);

$user = $user_gateway->getByUsername($data['username']);

if ($user === false) {
    http_response_code(401);
    echo json_encode(["message" => "invalid authentication"]);
    exit;
}

if (!password_verify($data['password'], $user['password_hash'])) {
    http_response_code(401);
    echo json_encode(["message" => "invalid authentication"]);
    exit;
}


require __DIR__ . "/tokens.php";

$refresh_token_gateway = new RefreshTokenGateway($database, $_ENV["SECRET_KEY"]);

$refresh_token_gateway->create($refresh_token, $refresh_token_expiry);

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

  • Пожалуйста, перейдите к нашему пользовательскому интерфейсу для регистрации пользователей. URL-адрес должен быть http://localhost/php_auth_jwt_tut/register.php, если вы следовали структуре проекта.
  • Отправьте имя пользователя и пароль созданного пользователя в конечную точку входа (http://localhost/php_auth_jwt_tut/api/login.php) в формате JSON, как показано ниже, и выполните запрос:
Тестирование конечных точек
Тестирование конечных точек

Вуаля! Вот так мы получаем наш токен JWT!

Защищенные ресурсы и декодирование веб-токенов JSON

Успешно сгенерировав токен JWT, давайте теперь рассмотрим, как мы можем защитить наши ресурсы и проверить содержимое токена путем его декодирования.

Вы обнаружите, что наши классы контроллера StudentController.php и класс шлюза StudentGateway.php, расположенный в папке src, уже настроены с использованием основных методов. Теперь все, что осталось, — это создать экземпляры этих классов в нашем index.php, который будет служить точкой входа для наших запросов в папке API.

Для этого добавьте следующий фрагмент кода в файл index.php, расположенный в папке API нашего проекта:

<?php

require __DIR__ . '/bootstrap.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    header('ALLOW: POST');
    exit();
}

$contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';


if ($contentType !== 'application/json') {
    http_response_code(415);
    echo json_encode(["message" => "Only JSON content is supported"]);
    exit();
}

$data = json_decode(file_get_contents('php://input'), true);

if ($data === null) {
    http_response_code(400);
    echo json_encode(["message" => "Invalid JSON data"]);
    exit();
}


if (!array_key_exists('username', $data) || !array_key_exists('password', $data)) {
    http_response_code(400);
    echo json_encode(["message" => "Missing login credentials"]);
    exit();
}

$user_gateway = new UserGateway($database);

$user = $user_gateway->getByUsername($data['username']);

if ($user === false) {
    http_response_code(401);
    echo json_encode(["message" => "invalid authentication"]);
    exit;
}

if (!password_verify($data['password'], $user['password_hash'])) {
    http_response_code(401);
    echo json_encode(["message" => "invalid authentication"]);
    exit;
}


$payload = [
    "id" => $user['id'],
    "name" => $user["name"]
];


$JwtController = new Jwt($_ENV["SECRET_KEY"]);

$token =$JwtController->encode($payload);

echo json_encode(["token" => $token]);

$user = new UserGateway($database);


$gateway = new StudentGateway($database);

$controller = new StudentController($gateway);


$controller->processRequest($_SERVER['REQUEST_METHOD']);

Как структурированы файлы на данном этапе:

файловая структура в VS Code
файловая структура в VS Code

Обратите внимание, что при тестировании конечной точки (http://localhost/phpAuthJWT/api) на этом этапе вы получите следующий результат без каких-либо ограничений:

тестирование конечных точек
тестирование конечных точек
Примечание. После настройки подключения к базе данных и импорта предоставленного файла College.sql в соответствии с инструкциями таблицы, включая таблицу «студенты», заполняются предварительно добавленными данными. Это позволяет нам просматривать данные об учениках, которые видны в данный момент.

Что происходит сейчас?

Мы устанавливаем собственную конечную точку URL-адреса для получения данных с помощью метода GET HTTP. URL-адрес http://localhost/phpAuthJWT/api/getAllStudents останется работоспособным благодаря конфигурации сервера, указанной в нашем файле .htaccess, расположенном в папке API. При выполнении запросов к этому URL-адресу мы будем включать наш токен в заголовок, используя стандартный заголовок запроса HTTP-авторизации, придерживаясь формата токена-носителя:

Разрешение: на предъявителя

В нашей папке src мы настроили класс Auth, в котором создадим метод проверки токена JWT. Этот метод проверяет, указан ли токен в заголовке HTTP, и декодирует его.

Как настроить метод декодирования в нашем классе JWT

При настройке функциональности декодирования в классе Jwt, как мы это делали с методом кодирования, мы обеспечим, чтобы наш класс Jwt, расположенный в папке src нашего проекта в файле jwt.php, справился с этой задачей. Для этого мы включим следующие фрагменты кода в наш класс Jwt — новые методы — это методы decode и base64UrlDecode, тем самым завершив его структуру. Окончательный код будет выглядеть следующим образом:

class Jwt
{

    public function __construct(private string $key)
    {

    }

    public function encode(array $payload): string
    {

        $header = json_encode([
            "alg" => "HS256",
            "typ" => "JWT"
        ]);

        $header = $this->base64URLEncode($header);
        $payload = json_encode($payload);
        $payload = $this->base64URLEncode($payload);

        $signature = hash_hmac("sha256", $header . "." . $payload, $this->key, true);
        $signature = $this->base64URLEncode($signature);
        return $header . "." . $payload . "." . $signature;
    }


   
    public function decode(string $token): array
    {
        if (
            preg_match(
                "/^(?<header>.+)\.(?<payload>.+)\.(?<signature>.+)$/",
                $token,
                $matches
            ) !== 1
        ) {

            throw new InvalidArgumentException("invalid token format");
        }

        $signature = hash_hmac(
            "sha256",
            $matches["header"] . "." . $matches["payload"],
            $this->key,
            true
        );

        $signature_from_token = $this->base64URLDecode($matches["signature"]);

        if (!hash_equals($signature, $signature_from_token)) {

            // throw new Exception("signature doesn't match");
            throw new InvalidSignatureException;
        }

        $payload = json_decode($this->base64URLDecode($matches["payload"]), true);

        return $payload;
    }

  
    private function base64URLEncode(string $text): string
    {

        return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($text));
    }

    private function base64URLDecode(string $text): string
    {
        return base64_decode(
            str_replace(
                ["-", "_"],
                ["+", "/"],
                $text
            )
        );
    }

  
}

Метод декодирования:

Метод decodeотвечает за декодирование токена JWT в соответствующие компоненты заголовка и полезной нагрузки. Вот краткий обзор того, что он делает:

  • Проверка токена: во-первых, он проверяет, соответствует ли предоставленный токен ожидаемому формату трех разделов, разделенных точками.
  • Проверка подписи: он пересчитывает подпись на основе заголовка и полезных данных токена и сравнивает ее с подписью, представленной в токене. Этот шаг обеспечивает целостность токена.
  • Извлечение полезных данных: если проверка подписи проходит успешно, компонент полезных данных токена декодируется из кодировки URL-адреса base64 в формат JSON. Эта декодированная полезная нагрузка содержит информацию о пользователе, связанном с токеном.
  • Наконец, он возвращает декодированные полезные данные в виде ассоциативного массива.

Метод base64URLDecode:

Метод base64URLDecode — это вспомогательная функция, используемая специально для декодирования строк, закодированных с использованием кодировки URL-адресов Base64. Вот разбивка его функционала:

  • Замена символов: сначала символы - и _ в закодированной строке заменяются на + и / соответственно. Этот шаг необходим, поскольку кодировка URL-адреса заменяет определенные символы для безопасной передачи через Интернет.
  • Base64 Decoding: после замены символов выполняется стандартная операция декодирования Base64 над измененной строкой.
  • Наконец, он возвращает декодированную строку.

Таким образом, метод декодирования проверяет и извлекает полезную нагрузку из токена JWT, а метод base64URLDecode помогает декодировать строки, закодированные с использованием кодировки URL-адреса base64, обеспечивая целостность и точность декодированных данных.

Защищенные ресурсы

Теперь, когда мы завершили настройку, давайте завершим наш класс Jwt. Крайне важно ограничить доступ к нашим конечным точкам без обязательного заголовка авторизации. Например, доступ к URL-адресу http://localhost/phpAuthJWT/api/getAllStudents должен быть ограничен, если необходимый заголовок авторизации отсутствует, а доступ к ресурсам должен быть запрещен, если URL-адрес неверен.

Для этого добавьте следующий фрагмент кода в начало вашего файла index.php, который служит точкой входа для доступа к нашим данным об учащихся. Вставьте этот код после импорта файла конфигурации bootstrap.php.

require __DIR__ . '/bootstrap.php';

$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);

$parts = explode("/", $path);


$resource = $parts[3];

$id = $parts[4] ?? null;

if ($resource != "getAllStudents") {

    http_response_code(404);
    exit;
}

В этом фрагменте показано, как мы извлекли путь из запрошенного URI с помощью функции parse_url(), изолируя конечную точку, к которой обращается пользователь. Затем разбиваем путь на сегменты с помощью функции explode(), что позволяет нам идентифицировать запрошенный ресурс. Если запрошенный ресурс не getAllStudents, что указывает на недопустимую конечную точку, код отвечает кодом состояния 404 Not Found, сигнализируя, что запрошенный ресурс не существует. Это гарантирует доступ только к действительным конечным точкам, предотвращая несанкционированный доступ к ресурсам нашего API.

Примечание. Чтобы убедиться, что наш проект работает должным образом, отправьте запрос на конечную точку getAllStudents по адресу http://localhost/phpAuthJWT/api/getAllStudents.

Этот запрос поможет нам убедиться, что наш API правильно настроен и способен получать все данные учащихся.

Проверка заголовков на корректность схемы авторизации

Мы установили собственный URL-адрес, но очень важно убедиться, что токен JWT, указанный в заголовке, действителен. Для этого мы будем использовать класс Auth, который уже доступен в папке src нашего проекта. Этот класс, предоставленный в шаблоне нашего проекта, поможет обеспечить целостность токена JWT.

Авторизация.php

class Auth
{



    public function __construct(private UserGateway $user_gateway, private Jwt $JwtCtrl)
    {
    }



    public function authenticateJWTToken(): bool
    {

        if (!preg_match("/^Bearer\s+(.*)$/", $_SERVER["HTTP_AUTHORIZATION"], $matches)) {
            http_response_code(400);
            echo json_encode(["message" => "incomplete authorization header"]);
            return false;
        }

        try {
            $data = $this->JwtCtrl->decode($matches[1]);
        } catch (InvalidSignatureException) {

            http_response_code(401);
            echo json_encode(["message" => "invalid signature"]);
            return false;
        } catch (Exception $e) {

            http_response_code(400);
            echo json_encode(["message" => $e->getMessage()]);
            return false;
        }



        return true;
    }
}

Этот класс отвечает за обработку аутентификации токенов JWT. Внутри класса у нас есть метод-конструктор, который инициализирует объект Auth экземплярами двух других классов: UserGateway и Jwt.

Прекрасно! Давайте разберем фрагмент кода более подробно и повествовательно:

В предоставленном фрагменте кода PHP мы определили класс с именем Auth. Этот класс отвечает за обработку аутентификации токенов JWT. Внутри класса у нас есть метод-конструктор, который инициализирует объект Auth экземплярами двух других классов: UserGateway и Jwt.

class Auth
{
    public function __construct(private UserGateway $user_gateway, private Jwt $JwtCtrl)
    {
    }
    // Other methods will go here...
}

Метод конструктора позволяет классу Auth взаимодействовать с пользовательскими данными через класс UserGateway и обрабатывать токены JWT с помощью класса Jwt.

Далее у нас есть метод authenticateJWTToken(), задачей которого является проверка действительности токена JWT, присутствующего в заголовке авторизации HTTP входящих запросов.

public function authenticateJWTToken(): bool
{
    
}

В методе authenticateJWTToken() код сначала проверяет, правильно ли отформатирован заголовок авторизации и содержит ли действительный токен JWT.

if (!preg_match("/^Bearer\s+(.*)$/", $_SERVER["HTTP_AUTHORIZATION"], $matches)) {
    http_response_code(400);
    echo json_encode(["message" => "incomplete authorization header"]);
    return false;
}

Если заголовок авторизации неполный или неправильно отформатирован, метод возвращает ответ 400 Bad Request вместе с сообщением, указывающим на проблему.

Затем код пытается декодировать токен JWT, используя метод decode() класса Jwt. Если процесс декодирования завершается неудачей из-за недопустимой подписи или любого другого исключения, возвращаются соответствующие коды ответов HTTP и сообщения об ошибках.

try {
    $data = $this->JwtCtrl->decode($matches[1]);
} catch (InvalidSignatureException) {
    http_response_code(401);
    echo json_encode(["message" => "invalid signature"]);
    return false;
} catch (Exception $e) {
    http_response_code(400);
    echo json_encode(["message" => $e->getMessage()]);
    return false;
}

Если токен JWT успешно декодирован без каких-либо исключений, метод возвращает true, указывая, что процесс аутентификации прошел успешно.

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

Создание экземпляров в точке входа API

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

index.php:


declare(strict_types=1);

require __DIR__ . '/bootstrap.php';

$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);

$parts = explode("/", $path);


$resource = $parts[3];

if ($resource != "getAllStudents") {

    http_response_code(404);
    exit;
}


$user = new UserGateway($database);


$JwtCtrl = new Jwt($_ENV["SECRET_KEY"]);

$auth = new Auth($user, $JwtCtrl);

if (!$auth->authenticateJWTToken()) {
    exit;
}



$gateway = new StudentGateway($database);

$controller = new StudentController($gateway);



$controller->processRequest($_SERVER['REQUEST_METHOD']);

После того как мы включили предоставленный фрагмент в наш index.php, очень важно наблюдать за различными сценариями, которые происходят при доступе к нашей конечной точке API по адресу http://localhost/phpAuthJWT/api/getAllStudents.

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

  • Успешный ответ. Если процесс аутентификации прошел успешно и токен JWT действителен, API должен вернуть ответ, содержащий нужные данные, например список всех студентов.
  • Неверный токен. Если токен JWT, указанный в заголовке авторизации запроса, недействителен, просрочен или имеет неправильный формат, API должен ответить сообщением об ошибке, указывающим на проблему. Это гарантирует, что только авторизованные пользователи смогут получить доступ к защищенным ресурсам.
  • Несанкционированный доступ. Если запрос не включает токен JWT или не имеет надлежащей авторизации, API должен ответить кодом состояния 401 «Неавторизованный», указывающим, что доступ к запрошенному ресурсу ограничен.
  • Неверная конечная точка. Если предоставленный URL-адрес не соответствует ни одной из определенных конечных точек или маршрутов в нашем приложении, API должен ответить кодом состояния 404 Not Found, сигнализирующим о том, что запрошенный ресурс не существует.

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

Правильная работа с заголовком авторизации: http://localhost/phpAuthJWT/api/getAllStudents

Не работает должным образом без заголовка авторизации: http://localhost/phpAuthJWT/api/getAllStudents

Предложения

После настройки JWT перед нами открывается совершенно новый мир. Вы можете поэкспериментировать с нашим API, создав собственные URL-адреса для конкретных задач, таких как поиск учащихся по имени или предоставление пользователям возможности создавать и обновлять свои собственные профили учащихся, отслеживая user_id пользователя. Вы можете использовать различные методы HTTP, такие как GET, POST, PATCH и DELETE, для эффективного управления данными. Хотя в этой статье рассматриваются основы, существует множество возможностей, которые предстоит изучить, когда дело доходит до создания надежного API.

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

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

Мы изучили реализацию веб-токенов JSON (JWT) и научились их использовать. Однако сохраняется серьезная проблема безопасности: наши токены, в данном контексте называемые токенами доступа, в настоящее время могут иметь неограниченный доступ к нашим ресурсам. Такая практика не соответствует отраслевым стандартам. Чтобы повысить безопасность, мы должны реализовать истечение срока действия токенов для наших JWT. Кроме того, рекомендуется использовать систему с двумя токенами, включающую токен обновления вместе с нашим токеном доступа.

Как реализовать срок действия токена

Мы сосредоточимся на реализации токенов доступа и токенов обновления. Любые необходимые дополнения к нашему проекту или структуре базы данных будут вноситься по мере реализации.

Давайте приступим прямо к делу!

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

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

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

<?php
declare(strict_types=1);

require __DIR__ . "/bootstrap.php";

if ($_SERVER["REQUEST_METHOD"] !== "POST") {
    
    http_response_code(405);
    header("Allow: POST");
    exit;
}

$data = (array) json_decode(file_get_contents("php://input"), true);

if ( ! array_key_exists("token", $data)) {

    http_response_code(400);
    echo json_encode(["message" => "missing token"]);
    exit;
}

$JwtController = new Jwt($_ENV["SECRET_KEY"]);
<?php
declare(strict_types=1);

require __DIR__ . "/bootstrap.php";

if ($_SERVER["REQUEST_METHOD"] !== "POST") {
    
    http_response_code(405);
    header("Allow: POST");
    exit;
}

$data = (array) json_decode(file_get_contents("php://input"), true);

if ( ! array_key_exists("token", $data)) {

    http_response_code(400);
    echo json_encode(["message" => "missing token"]);
    exit;
}

$JwtController = new Jwt($_ENV["SECRET_KEY"]);

Он проверяет, является ли метод входящего запроса POST; в противном случае он отвечает кодом состояния HTTP 405 и разрешает только запросы POST. Затем скрипт анализирует данные JSON из тела запроса, гарантируя, что они содержат ключ «токена». Если токен отсутствует, он отвечает кодом состояния 400 и сообщением JSON, указывающим на отсутствие токена. Наконец, сценарий инициализирует объект Jwt секретным ключом из переменных среды для дальнейшей обработки токена.

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

Класс RefreshTokenGateway использует PDO-соединение и секретный ключ для хеширования токенов. Его конструктор инициализирует соединение с базой данных и переменные секретного ключа. Метод create генерирует хеш для токена и вставляет его в таблицу refresh_token вместе со сроком его действия. Метод удаления удаляет токен из базы данных на основе его хеш-значения. Метод getByToken получает сведения о токене на основе его хеша. Наконец, метод deleteExpired удаляет токены с истекшим сроком действия из базы данных, обеспечивая эффективное управление токенами.

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

RefeshTokenGateway.php

<?php

class RefreshTokenGateway
{
    private PDO $conn;
    private string $key;
    
    public function __construct(Database $database, string $key)
    {
        $this->conn = $database->getConnection();
        $this->key = $key;
    }
    
    public function create(string $token, int $expiry): bool
    {
        $hash = hash_hmac("sha256", $token, $this->key);
        
        $sql = "INSERT INTO refresh_token (token_hash, expires_at)
                VALUES (:token_hash, :expires_at)";
                
        $stmt = $this->conn->prepare($sql);
        
        $stmt->bindValue(":token_hash", $hash, PDO::PARAM_STR);
        $stmt->bindValue(":expires_at", $expiry, PDO::PARAM_INT);
        
        return $stmt->execute();
    }
    
    public function delete(string $token): int
    {
        $hash = hash_hmac("sha256", $token, $this->key);
        
        $sql = "DELETE FROM refresh_token
                WHERE token_hash = :token_hash";
                
        $stmt = $this->conn->prepare($sql);
        
        $stmt->bindValue(":token_hash", $hash, PDO::PARAM_STR);
        
        $stmt->execute();
        
        return $stmt->rowCount();
    }
    
    public function getByToken(string $token): array | false
    {
        $hash = hash_hmac("sha256", $token, $this->key);
        
        $sql = "SELECT *
                FROM refresh_token
                WHERE token_hash = :token_hash";
                
        $stmt = $this->conn->prepare($sql);
        
        $stmt->bindValue(":token_hash", $hash, PDO::PARAM_STR);
        
        $stmt->execute();
        
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    
    public function deleteExpired(): int
    {
        $sql = "DELETE FROM refresh_token
                WHERE expires_at < UNIX_TIMESTAMP()";
            
        $stmt = $this->conn->query($sql);
        
        return $stmt->rowCount();
    }
}

Обновленная структура папок

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

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

Прежде чем мы закончим настройку конечной точки обновления для получения нового токена доступа после истечения срока действия, давайте интегрируем получение нашего токена обновления и токена доступа в нашу конечную точку входа в систему. У нас есть класс RefreshTokenGateway для управления токенами обновления, и мы создадим файл tokens.php в нашей папке API для создания токена, который будет использоваться как в конечных точках обновления, так и в конечных точках входа. Такой подход обеспечивает простой и унифицированный процесс управления токенами в нашем приложении.

tokens.php:

<?php

$payload = [
    "sub" => $user["id"],
    "name" => $user["name"],
    "exp" => time() + 20
];

$JwtController = new Jwt($_ENV["SECRET_KEY"]);

$access_token = $JwtController->encode($payload);

$refresh_token_expiry = time() + 432000;

$refresh_token = $JwtController->encode([
    "sub" => $user["id"],
    "exp" => $refresh_token_expiry
]);

echo json_encode([
    "access_token" => $access_token,
    "refresh_token" => $refresh_token
]);

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

login.php:

<?php

require __DIR__ . '/bootstrap.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    header('ALLOW: POST');
    exit();
}

$contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';


if ($contentType !== 'application/json') {
    http_response_code(415);
    echo json_encode(["message" => "Only JSON content is supported"]);
    exit();
}

$data = json_decode(file_get_contents('php://input'), true);

if ($data === null) {
    http_response_code(400);
    echo json_encode(["message" => "Invalid JSON data"]);
    exit();
}


if (!array_key_exists('username', $data) || !array_key_exists('password', $data)) {
    http_response_code(400);
    echo json_encode(["message" => "Missing login credentials"]);
    exit();
}

$user_gateway = new UserGateway($database);

$user = $user_gateway->getByUsername($data['username']);

if ($user === false) {
    http_response_code(401);
    echo json_encode(["message" => "invalid authentication"]);
    exit;
}

if (!password_verify($data['password'], $user['password_hash'])) {
    http_response_code(401);
    echo json_encode(["message" => "invalid authentication"]);
    exit;
}


require __DIR__ . "/tokens.php";

$refresh_token_gateway = new RefreshTokenGateway($database, $_ENV["SECRET_KEY"]);

$refresh_token_gateway->create($refresh_token, $refresh_token_expiry);
Примечание. Прежде чем двигаться дальше, давайте обновим наш класс Jwt, в частности функцию декодирования. Обновленная функция декодирования в классе Jwt проверяет, имеет ли токен правильный формат, гарантируя наличие заголовка, полезных данных и подписи. Затем он проверяет подпись токена, чтобы убедиться, что он не был подделан. После этого он декодирует полезную нагрузку (содержащую информацию о пользователе и срок действия) из токена. Наконец, он проверяет, истек ли срок действия токена, и выдает ошибку, если это так. В целом, эти шаги гарантируют, что токен действителен, неизменен и находится в пределах срока действия для безопасного использования в приложении.

Класс JWT

<?php

class Jwt
{

    public function __construct(private string $key)
    {

    }

    public function encode(array $payload): string
    {

        $header = json_encode([
            "alg" => "HS256",
            "typ" => "JWT"
        ]);

        $header = $this->base64URLEncode($header);
        $payload = json_encode($payload);
        $payload = $this->base64URLEncode($payload);

        $signature = hash_hmac("sha256", $header . "." . $payload, $this->key, true);
        $signature = $this->base64URLEncode($signature);
        return $header . "." . $payload . "." . $signature;
    }


   
    public function decode(string $token): array
    {
        if (preg_match("/^(?<header>.+)\.(?<payload>.+)\.(?<signature>.+)$/",
                   $token,
                   $matches) !== 1) {
                       
            throw new InvalidArgumentException("invalid token format");
        }
        
        $signature = hash_hmac("sha256",
                               $matches["header"] . "." . $matches["payload"],
                               $this->key,
                               true);   
                               
        $signature_from_token = $this->base64urlDecode($matches["signature"]);
        
        if ( ! hash_equals($signature, $signature_from_token)) {
            
            throw new InvalidSignatureException;
        }
        
        $payload = json_decode($this->base64urlDecode($matches["payload"]), true);
        
        if ($payload["exp"] < time()) {
            
            throw new TokenExpiredException;
        }
        
        return $payload;
    }
    
  
    private function base64URLEncode(string $text): string
    {

        return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($text));
    }

    private function base64URLDecode(string $text): string
    {
        return base64_decode(
            str_replace(
                ["-", "_"],
                ["+", "/"],
                $text
            )
        );
    }

  
}

Обновлен класс UserGateway.php

<?php

class UserGateway
{
    private PDO $conn;
    
    public function __construct(Database $database)
    {
        $this->conn = $database->getConnection();
    }
    
    public function getByAPIKey(string $key): array | false
    {
        $sql = "SELECT *
                FROM user
                WHERE api_key = :api_key";
                
        $stmt = $this->conn->prepare($sql);
        
        $stmt->bindValue(":api_key", $key, PDO::PARAM_STR);
        
        $stmt->execute();
        
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    
    public function getByUsername(string $username): array | false
    {
        $sql = "SELECT *
                FROM user
                WHERE username = :username";
                
        $stmt = $this->conn->prepare($sql);
        
        $stmt->bindValue(":username", $username, PDO::PARAM_STR);
        
        $stmt->execute();
        
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    
    public function getByID(int $id): array | false
    {
        $sql = "SELECT *
                FROM user
                WHERE id = :id";
                
        $stmt = $this->conn->prepare($sql);
        
        $stmt->bindValue(":id", $id, PDO::PARAM_INT);
        
        $stmt->execute();
        
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
}

Завершение конечной точки обновления

<?php
declare(strict_types=1);

require __DIR__ . "/bootstrap.php";

if ($_SERVER["REQUEST_METHOD"] !== "POST") {
    
    http_response_code(405);
    header("Allow: POST");
    exit;
}

$data = (array) json_decode(file_get_contents("php://input"), true);

if ( ! array_key_exists("token", $data)) {

    http_response_code(400);
    echo json_encode(["message" => "missing token"]);
    exit;
}

$JwtController = new Jwt($_ENV["SECRET_KEY"]);

try {
    $payload = $JwtController->decode($data["token"]);
    
} catch (Exception) {
    
    http_response_code(400);
    echo json_encode(["message" => "invalid token"]);
    exit;
}

$user_id = $payload["sub"];


$refresh_token_gateway = new RefreshTokenGateway($database, $_ENV["SECRET_KEY"]);

$refresh_token = $refresh_token_gateway->getByToken($data["token"]);

if ($refresh_token === false) {
    
    http_response_code(400);
    echo json_encode(["message" => "invalid token (not on whitelist)"]);
    exit;
}
                         
$user_gateway = new UserGateway($database);

$user = $user_gateway->getByID($user_id);

if ($user === false) {
    
    http_response_code(401);
    echo json_encode(["message" => "invalid authentication"]);
    exit;
}

require __DIR__ . "/tokens.php";

$refresh_token_gateway->delete($data["token"]);

$refresh_token_gateway->create($refresh_token, $refresh_token_expiry);

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

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

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

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

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

Заключение

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

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

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

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

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

Полный код смотрите здесь.

Источник:

#PHP #JSON
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу