Вступление

Qt5.8 принёс нам QNetworkAuthorization -- модуль авторизации на сторонних сервисах. Пока что поддерживаются только протоколы OAuth и OAuth2.0, но обещали позже подвезти и OpenID.

В стандартной документации лежат два примера: для реддита и твиттера, также нашёлся пример работы с гуглом. Но т.к. Vk нам всем ближе, было решено запилить статью и для него.

OAuth2.0

Я не стану вдаваться в подробности протокола, за меня это уже неплохо сделали здесьздесь и вот здесь.

В своей статье я лишь кратко расставлю по местам действующие лица, которые понадобятся при написании кода:

  • Client -- программа, которую вы пишете, и которой очень нужно получить данные пользователя или действовать от его имени.

  • Resource owner -- он же user, т.е. владелец прав, аккаунтов и т.д.

  • Authorization server -- сущность, которая, после успешной авторизации, выдаст client-у токен, позволяющий ему в течение некоторого времени работать от имени user-а.

  • Resource server -- сервер с защищёнными данными (собственно, сервер Vk, на который будут отправляться запросы).

Установка QNetworkAuthorization

  1. Включаем vpn (без него сейчас Qt никак не пошевелить), я использую Windscribe или Tunnelbear.

  2. Устанавливаем модуль. Я использую Qt6.4.0-beta.

  3. Также советую установить OpenSsl (в самом низу списка), чтобы потом не ставить его отдельно для libcryptho.dll. Если у вас в системе уже где-то есть OpenSsl, то эту галочку можно не прожимать.

Создание приложения Vk (client-а)

Чтобы Client мог обращаться к authorization server-у, ему нужны client_id и client_secret.

  • client_id -- это уникальный публичный идентификатор клиента.

  • client_secret -- ключ, который должен быть полностью секьюрен. Позволяет подтвердить, что под этим client_id не скрывается некто иной, например, фишинговый сайт мошенника.

Можно рассматривать client_id как логин вашего приложения на сервере авторизации, а client_secret -- как пароль.

Для начала нужно перейти на страницу менеджмента приложений и авторизоваться на ней.

Нажимаем кнопку “Создать”.

Выбираем Standalone-приложение и вводим название:

Тыкаем «Подключить приложение», вводим код из смс и переходим на вкладку «Настройки».

Для начала нужно запомнить два поля: «ID приложения» (client_id), и «Защищённый ключ» (client_secret). Нажимаем на иконку глаза справа от звёздочек, вводим код и перезагружаем страницу. Также нужно сменить настройку «Состояние» на «Приложение включено и видно всем».

Теперь нужно настроить параметры OAuth. Для этого включаем Open API, это позволит использовать Authorization Code Flow.

Если у вас есть один или несколько IP, через который будет проводиться авторизация пользователей, то эти IP необходимо ввети во вкладку “Базовый домен”, но т.к. это чисто тестовое приложение, то авторизовываться мы в нём буду сразу с клиента, поэтому в “Базовый домен будет“127.0.0.1”. То же самое введу в поле “Адрес сайта” в качестве затычки.

Жмём “Сохранить изменения”, перезагружаем страницу и в появившемся поле “Доверенный redirect uri” вводим адрес, на который будет отправляться запрос с кодом, по которому позже можно будет взять токен у authorization server. В этом приложении весь путь авторизации идёт на клиенте, а потому вводим “http://127.0.0.1:6543/”. Сохраняем ещё раз, и можно закрывать страницу.

Собственно, код

Запускаем IDE, создаём “Приложение Qt Widgets”. На Qt5 лучше брать связку из MinGW + qmake. Для Qt6 лучше подходит msvc + CMake.

Добавляем в CMakeList.txt:
find_package(Qt6 REQUIRED COMPONENTS NetworkAuth)
target_link_libraries(VkOAuth2Project PRIVATE Qt6::NetworkAuth)

Если у вас qmake, достаточно написать в pro-файле
QT += networkauth

Далее возьмём из документации Vk некоторые ссылки и значения:

    const QUrl authUrl{ "https://oauth.vk.com/authorize" };
    const QUrl tokenUrl{ "https://oauth.vk.com/access_token" };
    const QString clientSecret{ "d4rZuR8zHiYHgntXC1kp" };
    const QString clientId{ "51400815" };
    constexpr quint32 scopeMask = 2;//https://dev.vk.com/reference/access-rights

Этап 1. Начальная настройка

Для начала нужно создать объекты QOAuth2AuthorizationCodeFlow и QOAuthHttpServerReplyHandler. Первый отвечает за весь цикл авторизации и последующие запросы, второй нужен, чтобы принять code и token от authorization server-a и создать redirect_uri.

    auto oauth = new QOAuth2AuthorizationCodeFlow(&mainWindow);
    auto replyHandler = new QOAuthHttpServerReplyHandler(6543, &mainWindow);

Обращаю внимание, что порт у replyHandler такой же, как в настройках приложения на сайте Vk. Это важно, т.к. этот объект вернёт redirect_uri формата http://127.0.0.1:<port>/. Это поведение можно изменить, переопределив в потомке QOAuthHttpServerReplyHandler виртуальный метод callback.

Далее заполняем oauth настройками:

    oauth->setReplyHandler(replyHandler);
    oauth->setAccessTokenUrl(tokenUrl);
    oauth->setAuthorizationUrl(authUrl);
    oauth->setClientIdentifier(clientId);
    oauth->setClientIdentifierSharedKey(clientSecret);
    oauth->setScope(QString::number(scopeMask));

Этап 2. Подтверждение доверия клиенту от пользователя

Resource owner должен подтвердить доверие client-у, после чего authorization server даст client-у одноразовый code. Для этого есть два пути.

QObject::connect(oauth, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, &QDesktopServices::openUrl);

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

Текст можно переопределить с помощью метода QOAuthHttpServerReplyHandler::setCallbackText. Можно даже без проблем возвращать html-страницу.

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

При этом стоит учитывать, что модуль QWebEngine не работает с компилятором mingw (по крайней мере, так было, когда я тестил), да и сам модуль довольно тяжёлый, так что тянуть его ради одной только авторизации может быть неразумно.

Этап 3. Получение token из одноразового code

Здесь нам ничего делать не нужно, Qt сам обратится по адресу, переданному ему в tokenUrl за токеном, и уведомит нас об окончании процесса авторизации.

Что можно сделать после авторизации

После авторизации client полностью вправе действовать от лица пользователя в рамках установленного scope (набора прав). Мы установили scope = 2, что по документации означает «доступ к списку друзей». Поэтому мы может запросить у сервера Vk список друзей какого-либо пользователя.

//Сигнал granted излучается при успешной авторизации
QObject::connect(oauth, &QOAuth2AuthorizationCodeFlow::granted, [oauth]() {
        const QUrl getFriends{ "https://api.vk.com/method/friends.get" };
        
        //Vk требует указывать версию API в параметрах запроса. Укажем 5.131
        auto network_reply = oauth->post(getFriends, { { "v", 5.131 } });
        QObject::connect(network_reply, &QNetworkReply::finished, [network_reply] {
            //Ответ будет удалён позже, когда отработают все связанные 
            //с ним сигнально-слотовые соединения, 
            //поэтому такое удаление безопасно
            network_reply->deleteLater();

            QJsonDocument response = QJsonDocument::fromJson(network_reply->readAll());
            qDebug() << "All friends ids:";
            for(const auto& user_id : response["response"]["items"].toArray())
                qDebug() << '\t' << user_id.toInteger();
        });
    });

Запустим процесс авторизации:

oauth->grant();

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

Заключение

Развитие модуля на этом не заканчивается. Разработчики пока так и не завезли OpenID, а чтобы авторизовываться через сторонний сервер, вам придётся написать свою реализацию QAbstractOAuthReplyHandler, хотя со введением в Qt6.3 модуля QtHttpServer из QtLabs эта задача значительно облегчается.

Полный код доступен на GitHub

Комментарии (15)


  1. AllKnowerHou
    23.08.2022 16:40

    Зачем?


    1. 0xd34df00d
      23.08.2022 18:00
      +4

      Чтобы писать десктопные приложения, работающие с ВК, например.


      1. AllKnowerHou
        23.08.2022 19:03
        -5

        А какой толк, если это не безопасно


        1. 0xd34df00d
          23.08.2022 19:19
          +3

          Что небезопасно?


          1. AllKnowerHou
            23.08.2022 20:49
            -7

            Писать десктопные приложения


            1. 0xd34df00d
              24.08.2022 07:04
              +5

              Так что именно? Вот я открываю clion, пишу какой-то код на плюсах и кутях. Что тут небезопасно?


            1. MagMagals
              24.08.2022 09:00
              +5

              снимите розовые очки вебсервисов, в мире еще есть нужда в десктопных приложений


  1. HemulGM
    23.08.2022 16:47
    -1

    Не хочу быть "затычкой каждой дырке", но должен заметить, что в "мертвом" Делфи, это реализовано лет 5 назад)
    Смотрим компоненты для работы с REST

    Hidden text


    1. KanuTaH
      23.08.2022 17:14
      +4

      Qt 5.8 вышел в январе 2017-го, пять с половиной лет назад.


      1. HemulGM
        23.08.2022 18:45
        +1

        Виноват. Я не учел возраст минимальной версии. Однако, хочу исправиться
        REST пакет в Delphi имеется как минимум с 2013 года
        https://webdelphi.ru/2013/12/delphi-xe5-eshhyo-odin-primer-ispolzovaniya-rest-client-library/


        1. KanuTaH
          24.08.2022 11:20
          +3

          По Qt-аналогам того, что используется конкретно в этом примере: QHttp был ещё в Qt 3.x (это начало нулевых), в более поздних версиях его заменили на QNetworkAccessManager. Причём оба были изначально асинхронные, а в примере по вашей ссылке Execute блокирующий, насколько я понимаю (на Delphi не писал уже сто лет, не знаю, как там с этим было в 2013-м, если реально не было асинхронного варианта сделать REST-запрос без запуска отдельного потока, то это никуда не годится). QJsonObject с присными появился в Qt 5.0, это 2012 год, но ещё в Qt 4.7 (2010 год) был QML с полагающимся в ECMAScript-языке JSON.parse().


          1. HemulGM
            24.08.2022 16:15
            +1

            Execute  - блокирующий, однако там есть ExecuteAsync.
            У THTTPClient есть как блокирующие методы, так и всякие BeginGet/BeginPost и т.д. - не блокирующие.
            Помимо работы с REST, эта библиотека имеет сериализатор/десериализатор JSON.

            var MyObject := TJson.JsonToObject<TMyObject>('{ "field": "dd", "field2": 123 }');
            var JSON := TJson.ObjectToJsonString(MyObject);

            Да и создавать потоки не нужно созданием класса потока (TThread), достаточно написать

            TTask.Run(
              procedure
              begin
                HTTP.Get(Url)...
              end);


  1. Palm1r
    25.08.2022 15:10
    +1

    Напомню, что данный модуль(Qt Network Authorization) распространяется под Commercial и GPLv3, учитывайте это если хотите тащить это в проприетарный код


    1. TheGast Автор
      26.08.2022 07:47

      Так по GPLv3 почти весь Qt распространяется, даже виджеты (они, правда, ещё и по GPLv2 распространяются, но тем не менее).


      1. Palm1r
        26.08.2022 09:00

        Нет, это не совсем так. Почти весь Qt распространяется под LGPLv3(и даже виджеты), он позволяет динамически линковаться в том числе и с проприетарным кодом, с GPLv3 так нельзя. В общем надо смотреть, что вы используете и какая лицензия