Введение
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-адреса чаще логируются (в логах сервера, прокси, истории браузера), что увеличивает риск утечки токена.