Работа с сетью в Qt

Авторизация по токену (JWT) в HTTP-запросах через QNetworkRequest

Введение

JWT (JSON Web Token) — стандартный формат токенов для аутентификации в современных REST API: клиент получает токен после логина и передаёт его в каждом последующем запросе, обычно в заголовке Authorization: Bearer <token>. В этой статье рассмотрим, как добавлять JWT-токен к запросам через QNetworkRequest, как централизованно перехватывать все запросы для автоматической подстановки токена и как обрабатывать его истечение с автоматическим обновлением (refresh).

Концепция

Сам JWT — это просто строка из трёх частей, разделённых точками (заголовок, payload, подпись), закодированных в Base64Url. Qt не имеет встроенного класса для разбора или генерации JWT — для базовой задачи (просто передать токен на сервер) это и не требуется: достаточно один раз получить токен через эндпоинт логина и добавлять его в заголовок Authorization всех последующих запросов.

Чтобы не дублировать код подстановки заголовка в каждом месте, где выполняется HTTP-запрос, удобно создать обёртку над QNetworkAccessManager, которая автоматически добавляет заголовок авторизации ко всем запросам и перехватывает ответы с кодом 401 (Unauthorized), чтобы попытаться обновить токен через refresh-эндпоинт и повторить исходный запрос.

Пример кода: Авторизация по токену

#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDebug>

class AuthorizedApiClient : public QObject
{
    Q_OBJECT
public:
    explicit AuthorizedApiClient(QObject *parent = nullptr) : QObject(parent) {}

    void setAccessToken(const QString &token) { m_accessToken = token; }

    void getProfile()
    {
        request(QUrl("https://api.example.com/profile"), QByteArray());
    }

private:
    void request(const QUrl &url, const QByteArray &body, bool isRetry = false)
    {
        QNetworkRequest req(url);
        req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
        req.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());

        QNetworkReply *reply = body.isEmpty() ? m_manager.get(req)
                                               : m_manager.post(req, body);

        connect(reply, &QNetworkReply::finished, this, [this, reply, url, body, isRetry]() {
            onReplyFinished(reply, url, body, isRetry);
        });
    }

    void onReplyFinished(QNetworkReply *reply, const QUrl &url, const QByteArray &body, bool isRetry)
    {
        reply->deleteLater();

        const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();

        if (statusCode == 401 && !isRetry) {
            qDebug() << "Токен истёк, выполняю обновление";
            refreshToken([this, url, body]() {
                request(url, body, /*isRetry=*/true);
            });
            return;
        }

        if (reply->error() != QNetworkReply::NoError) {
            qWarning() << "Ошибка запроса:" << reply->errorString();
            return;
        }

        const auto doc = QJsonDocument::fromJson(reply->readAll());
        qDebug() << "Ответ:" << doc.toJson(QJsonDocument::Compact);
    }

    void refreshToken(std::function<void()> onSuccess)
    {
        QNetworkRequest req(QUrl("https://api.example.com/auth/refresh"));
        req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");

        QJsonObject payload;
        payload["refresh_token"] = m_refreshToken;
        QNetworkReply *reply = m_manager.post(req, QJsonDocument(payload).toJson());

        connect(reply, &QNetworkReply::finished, this, [this, reply, onSuccess]() {
            reply->deleteLater();
            if (reply->error() == QNetworkReply::NoError) {
                const auto obj = QJsonDocument::fromJson(reply->readAll()).object();
                m_accessToken = obj["access_token"].toString();
                onSuccess();
            } else {
                qWarning() << "Не удалось обновить токен, требуется повторный логин";
            }
        });
    }

    QNetworkAccessManager m_manager;
    QString m_accessToken;
    QString m_refreshToken;
};

Пояснения к коду

Метод request() — единая точка отправки всех запросов, которая добавляет заголовок Authorization с текущим access-токеном через setRawHeader(). В onReplyFinished() проверяется HTTP-статус ответа: код 401 (токен истёк или недействителен) запускает refreshToken(), и при успешном обновлении исходный запрос повторяется автоматически с флагом isRetry = true, чтобы не уйти в бесконечный цикл обновлений при повторном 401.

refreshToken() отправляет refresh-токен на отдельный эндпоинт и при успехе обновляет m_accessToken, вызывая переданный callback для продолжения исходной операции.

Подводные камни

  • Хранение access/refresh токенов в открытом виде в памяти или на диске. Для десктопных приложений рекомендуется использовать защищённое хранилище ОС (Windows Credential Manager, macOS Keychain, Linux Secret Service) — есть готовые Qt-обёртки на основе библиотеки QtKeychain.
  • Гонка при одновременных запросах с истёкшим токеном. Если несколько запросов параллельно получают 401, каждый запустит свой refreshToken(), что приведёт к множественным обращениям к серверу обновления токена и может инвалидировать предыдущие refresh-токены (если сервер реализует одноразовые refresh-токены). Нужна централизованная очередь ожидающих запросов на время обновления.
  • Бесконечный цикл при некорректной обработке isRetry. Если флаг повторной попытки не проверяется (как в примере) или сервер всегда возвращает 401 даже после обновления токена, без ограничения числа попыток код уйдёт в рекурсию.
  • Передача токена в URL вместо заголовка. Некоторые API поддерживают передачу токена как query-параметра — этого следует избегать, так как URL-адреса чаще логируются (в логах сервера, прокси, истории браузера), что увеличивает риск утечки токена.