Token-Based Authetification в автономных системах посредством Qt6 с использованием Qr-кодов. Http-сервер

от автора

Содержание

Общая схема

Хранить и отдавать сервер будет изображения из какой-либо директории (сама директория будет параметром командной строки).

Запросы с localhost будут проходить без авторизации, а для всех остальных будет проверяться http-заголовок Authorization на предмет наличия и валидности токена.

Для картинок будут две ручки: одна возвращает json-массив с именами картинок, вторая — картинку по имени.

Для токенов будет crud, доступный только по localhost.

Репозиторий изображений

Простой read-only репозиторий на два метода: получить список имён, и получить картинку по имени. Ничего лишнего.

Реализация ImageRepository
ImageRepository.h
namespace storages { class ImageRepository { private:     QDir m_root; public:     ImageRepository(const QString &root = QDir::rootPath()); public:     QImage image(const QString& name);     QStringList images() const; }; }

ImageRepository.cpp
namespace storages { ImageRepository::ImageRepository(const QString &root)     :m_root{ root } { m_root.setNameFilters(QStringList{} << "*.png" << "*.jpg" << "*.jpeg"); }  QImage ImageRepository::image(const QString &name) {     return QImage{ m_root.filePath(name) }; } QStringList ImageRepository::images() const {     return m_root.entryList(QDir::Filter::Files | QDir::Filter::Readable); } }

Репозиторий токенов

Тут всё малость сложнее: нам нужны возможности чтения и записи, а также проверки на валидность (existence и expiration). При этом нужно учитывать, что наш великий сервис может в любой момент упасть, поэтому нужно какое-то хранилище на диске.

В качестве токена будет выступать QUuid, время представим в виде QDateTime, а в качестве хранилища — старый добрый QSettings. Хранить будем в формате ключ-значение, где uuid — ключ, а expiration — значение.

Для этого определим 4 метода:

  • createToken — создаёт и возвращает новый токен доступа

  • removeToken — удаляет токен при его наличии

  • isValidToken — проверяет existence и expiration у токена

  • tokens — возвращает мапу token-expiration

Дополнительно при любом обращении должен вызываться метод removeExpiredTokens(), удаляющий все протухшие токены.

Реализация TokenRepository
TokenRepository.h
namespace storages { class TokenRepository { private:     static inline constexpr const char* s_tokens = "tokens";     QSettings m_storage; public:     TokenRepository(const QString& path = {}); public:     bool isValidToken(const QUuid& bearer);     QMap<QUuid, QDateTime> tokens(); public:     QUuid createToken(const QDateTime& expiration);     void removeToken(const QUuid& bearer); private:     void removeExpiredTokens(); }; }

TokenRepository.cpp
namespace storages { TokenRepository::TokenRepository(const QString &path)     :m_storage{ path, QSettings::Format::IniFormat } {          m_storage.beginGroup(s_tokens);      }   bool TokenRepository::isValidToken(const QUuid &token) {     removeExpiredTokens();     return m_storage.contains(token.toString()); } QMap<QUuid, QDateTime> TokenRepository::tokens() {     removeExpiredTokens();     QMap<QUuid, QDateTime> result{};     for(const auto& key: m_storage.allKeys())         result[QUuid::fromString(key)] = m_storage.value(key).toDateTime();      return result; }  QUuid TokenRepository::createToken(const QDateTime& expiration) {     removeExpiredTokens();     const auto token = QUuid::createUuid();     m_storage.setValue(token.toString(), expiration);     return token; } void TokenRepository::removeToken(const QUuid &token) {     removeExpiredTokens();     m_storage.remove(token.toString()); }  void TokenRepository::removeExpiredTokens() {     const auto current = QDateTime::currentDateTime();     for(const auto& key: m_storage.allKeys())         if(current > m_storage.value(key).toDateTime())             m_storage.remove(key); } }

Тут стоит отметить, что по CoreGuidelines следовало сделать методы isValidToken и tokens константными, а m_storage сделать mutable, дабы подчеркнуть логическую неизменность и отделить её от бинарной, но тут я решил этого не делать.

Контроллеры

Эти объекты нужны просто чтобы трансформировать данные из json в представление, которым пользуются репозитории. Ничего сложного.

Тут мы уже увидим использование класса QHttpServerResponse. Это класс, способный вернуть массив байтов, строку, json, короче всё, что должен уметь возвращать хороший http-сервер.

Реализация ImageController
ImageController.h
namespace controllers { class ImageController { private:     std::shared_ptr<storages::ImageRepository> m_images; public:     ImageController(const std::shared_ptr<storages::ImageRepository> &images); public:     QHttpServerResponse image(const QString& name) const;     QHttpServerResponse imagesList() const; }; }

ImageController.cpp
namespace controllers { ImageController::ImageController(const std::shared_ptr<storages::ImageRepository> &images)     :m_images{ std::move(images) } {}  QHttpServerResponse ImageController::image(const QString &name) const {     QByteArray result{};     //QBuffer - простой QIODevice для работы с QByteArray     QBuffer buffer{ &result };     m_images->image(name).save(&buffer, "PNG");     return QHttpServerResponse{ result }; } QHttpServerResponse ImageController::imagesList() const {     return QHttpServerResponse{ QJsonArray::fromStringList(m_images->images()) }; } }

Реализация TokenController
TokenController.h
namespace controllers { class TokenController { private:     std::shared_ptr<storages::TokenRepository> m_tokens; public:     TokenController(const std::shared_ptr<storages::TokenRepository> &tokens); public:     QHttpServerResponse createToken(quint64 expirationSpan);     QHttpServerResponse removeToken(const QByteArray& token);     QHttpServerResponse getAllTokens(); }; }

TokenController.cpp
namespace controllers { TokenController::TokenController(const std::shared_ptr<storages::TokenRepository> &tokens)     :m_tokens{ std::move(tokens) } { }  QHttpServerResponse TokenController::createToken(quint64 expirationSpan) {     return QHttpServerResponse{ m_tokens->createToken(QDateTime::currentDateTime().addSecs(expirationSpan)).toString() }; } QHttpServerResponse TokenController::removeToken(const QByteArray &bearer) {     m_tokens->removeToken(QUuid::fromString(bearer));     return QHttpServerResponse{ QHttpServerResponse::StatusCode::Accepted }; } QHttpServerResponse TokenController::getAllTokens() {     QJsonObject result{};     const auto elements = m_tokens->tokens();     for(auto iter = elements.begin(); iter != elements.end(); ++iter)         result[iter.key().toString(QUuid::StringFormat::WithoutBraces)] = iter.value().toSecsSinceEpoch();      return result; } }

Д

Для простоты json-интерфейса, в метод createToken нужно передавать число секунд, которое токен будет жить: если нужен токен, живущий сутки, нужно передать 24 * 60 * 60 = 86400.

Как видно, эти классы действительно не делают ничего, кроме перевода json-ов.

Http-фильтрация

Ну и, наверное, самая сложная часть этого сервиса. Ограничение доступа.

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

Для начала нам нужен фильтр. Для него введём два класса:

  • AbstractHttpController — контроллер, принимающий N параметров различных типов и возвращающий QHttpServerResponse.

  • TokenAuthorizator — собственно, фильтр, принимающий N параметров и QHttpServerRequest на конце.

AbstractHttpController.h
namespace utils { template<typename...Args> struct AbstractHttpController {     Q_DISABLE_COPY(AbstractHttpController);     virtual QHttpServerResponse handle(const Args&...) = 0;     virtual ~AbstractHttpController() = default;     AbstractHttpController() = default; }; }

Просто структура с дефолтными конструктором и деструктором и методом handle, принимающим variadic template.

TokenAuthorizator.h
namespace utils { template<typename...Args> class TokenAuthorizator: public AbstractHttpController<Args..., QHttpServerRequest> { private:     std::function<QHttpServerResponse(const Args&...)> m_next;     std::shared_ptr<storages::TokenRepository> m_tokens; public:     TokenAuthorizator(std::shared_ptr<storages::TokenRepository> tokens, const std::function<QHttpServerResponse(const Args&...)>& next)         :m_next{ std::move(next) }, m_tokens{ tokens } {} public:     virtual QHttpServerResponse handle(const Args&...parametes, const QHttpServerRequest& request) override {         if(not request.remoteAddress().isLoopback())             if(not m_tokens->isValidToken(QUuid::fromString(request.value("Authorization"))))                 return QHttpServerResponse::StatusCode::Unauthorized;          return m_next(parametes...);     } }; }

Тут уже несколько интереснее:

  1. Класс принимает Args… и QHttpServerRequest. Args… он передаёт дальше, а по QHttpServerRequest делает фильтрацию. В реальном коде нужна ещё специализация шаблона на случай, если QHttpServerRequest тоже нужно передавать.

  2. В методе handle сначала идёт проверка на то, что запрос пришёл не с ::1 (с этого ПК), а откуда-то извне. И если пришедший извне запрос не имеет валидного токена, возвращается code 401 (Unauthorized).

  3. В реальном коде не стоит передавать tokens в TokenAuthorizator напрямую. Стоит сделать прокладку в виде предиката, а уже tokens закидывать в этот предикат. Это удалит зависимость между этими классами.

Собрать всё в кучу

Осталось лишь соединить все эти куски вместе. Удобного Dependency Injection, как в C#, мы из коробки не имеем, да и тут он по большей части излишен. Поэтому соединяем прямо в main.

main.cpp
QCoreApplication app{ argc, argv };     if(app.arguments().size() == 2)         qFatal("Use app: <app-name> <image-dir> <tokens-storage>");  auto images = std::make_shared<ImageRepository>(argv[1]); auto tokens = std::make_shared<TokenRepository>(argv[2]); auto server = std::make_shared<QHttpServer>();  auto imageController = std::make_shared<ImageController>(images); auto tokenController = std::make_shared<TokenController>(tokens);  //можно воспользоваться std::bind или std::bind_from (since C++20) auto getAllTokens = std::make_shared<TokenAuthorizator<>>(tokens,     [tokenController]() { return tokenController->getAllTokens(); }); auto createToken = std::make_shared<TokenAuthorizator<quint64>>(tokens,     [tokenController](quint64 expiration) { return tokenController->createToken(expiration); }); auto removeToken = std::make_shared<TokenAuthorizator<QByteArray>>(tokens,     [tokenController](const QByteArray& token) { return tokenController->removeToken(token); });  auto getImagesList = std::make_shared<TokenAuthorizator<>>(tokens,     [imageController]() { return imageController->imagesList(); }); auto getImage = std::make_shared<TokenAuthorizator<QString>>(tokens,     [imageController](const QString& image) { return imageController->image(image); });  //Про api сервера и как пользоваться методом route можно //почитать тут: https://doc.qt.io/qt-6/qhttpserver.html server->route("/auth/token/all/", [getAllTokens](const QHttpServerRequest& request) {     return getAllTokens->handle(request); }); server->route("/auth/token/create/<arg>", [createToken](quint64 expirationSpan, const QHttpServerRequest& request) {     return createToken->handle(expirationSpan, request); }); server->route("/auth/token/remove/<arg>", [removeToken](const QByteArray& token, const QHttpServerRequest& request) {     return removeToken->handle(token, request); });  server->route("/data/images/list", [getImagesList](const QHttpServerRequest& request) {     return getImagesList->handle(request); }); server->route("/data/images/<arg>", [getImage](const QString& image, const QHttpServerRequest& request) {     return getImage->handle(image, request); });  //Отвечаем на запросы с любых адресов на порт 5555 server->listen(QHostAddress::SpecialAddress::Any, 5555); return app.exec();

Тестируем

Для теста достаточно двух устройств: на одном запустим сервер (ноутбук), с другого нужно делать запросы (качаем любой API-tester на мобилу и радуемся жизни).

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

Тест на ноуте
Список изображений

Список изображений
Список изображений

Запрос изображения

Чтобы связать ноутбук и телефон, достаточно раздать Wifi с одного устройства, и подключиться с другого.

Попробуем для начала сделать запрос без токена:

Как видим, не выходит. Теперь пробуем подключиться по токену:

Отправляем с доверенного устройства (ноутбука) запрос http://127.0.0.1:5555/auth/token/create/86400

В ответе придёт созданный токен

В ответе придёт созданный токен

Вставляем этот токен в хедер Authorization, и всё получается.

Авторизованный доступ

Авторизованный доступ

Заключение

В реальном проекте вы, скорее всего, будете использовать что-то типа OAuth. Для OAuth будет одно отличие: перед кодом будет слово Bearer.

Т.е.
Authorzation: 0d2c7f09-8a3a-4750-8d47-9a052bb1587f
превратится в
Authorzation: Bearer 0d2c7f09-8a3a-4750-8d47-9a052bb1587f

Но особой разницы в этом нет.


ссылка на оригинал статьи https://habr.com/ru/articles/733832/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *