Содержание
Общая схема
Хранить и отдавать сервер будет изображения из какой-либо директории (сама директория будет параметром командной строки).
Запросы с localhost будут проходить без авторизации, а для всех остальных будет проверяться http-заголовок Authorization на предмет наличия и валидности токена.
Для картинок будут две ручки: одна возвращает json-массив с именами картинок, вторая — картинку по имени.
Для токенов будет crud, доступный только по localhost.
![](https://habrastorage.org/getpro/habr/upload_files/a64/e03/189/a64e0318956eb502dec701da5d02dfdd.png)
Репозиторий изображений
Простой 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...); } }; }
Тут уже несколько интереснее:
-
Класс принимает Args… и QHttpServerRequest. Args… он передаёт дальше, а по QHttpServerRequest делает фильтрацию. В реальном коде нужна ещё специализация шаблона на случай, если QHttpServerRequest тоже нужно передавать.
-
В методе handle сначала идёт проверка на то, что запрос пришёл не с ::1 (с этого ПК), а откуда-то извне. И если пришедший извне запрос не имеет валидного токена, возвращается code 401 (Unauthorized).
-
В реальном коде не стоит передавать 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 на мобилу и радуемся жизни).
Для тестирования с ноута достаточно вбить запрос в строку браузера, и посмотреть, как всё это работает. Тут стоит отметить, что отдавать имена картинок — плохое решение, потому что в номерах будут пробелы. Но в реальном проекте все картинки вы, скорее всего, будете как-то индексировать, присваивать им имена и т.п.
Тест на ноуте
![Список изображений Список изображений](https://habrastorage.org/getpro/habr/upload_files/908/90a/e6b/90890ae6b742f2c4de054cbbcb63f3d1.png)
![Список изображений Список изображений](https://habrastorage.org/getpro/habr/upload_files/f56/c14/3c2/f56c143c221f0876193174c53292f162.png)
Чтобы связать ноутбук и телефон, достаточно раздать Wifi с одного устройства, и подключиться с другого.
Попробуем для начала сделать запрос без токена:
![](https://habrastorage.org/getpro/habr/upload_files/f3f/960/fdf/f3f960fdfc5d046a110947c58bee2bb6.png)
Как видим, не выходит. Теперь пробуем подключиться по токену:
Отправляем с доверенного устройства (ноутбука) запрос http://127.0.0.1:5555/auth/token/create/86400
![В ответе придёт созданный токен В ответе придёт созданный токен](https://habrastorage.org/getpro/habr/upload_files/c9f/5e8/290/c9f5e8290da07a937806d65affd55779.png)
Вставляем этот токен в хедер Authorization, и всё получается.
![Авторизованный доступ Авторизованный доступ](https://habrastorage.org/getpro/habr/upload_files/1a1/171/fad/1a1171fad4247175c0883a554280b76b.png)
Заключение
В реальном проекте вы, скорее всего, будете использовать что-то типа OAuth. Для OAuth будет одно отличие: перед кодом будет слово Bearer.
Т.е.
Authorzation: 0d2c7f09-8a3a-4750-8d47-9a052bb1587f
превратится в
Authorzation: Bearer 0d2c7f09-8a3a-4750-8d47-9a052bb1587f
Но особой разницы в этом нет.
ссылка на оригинал статьи https://habr.com/ru/articles/733832/
Добавить комментарий