Files
minecraft-launcher/doc/cpp/MsaAuthService.md
T
2026-09-03 09:16:56 +03:00

10 KiB

MsaAuthService

Обзор класса

MsaAuthService — авторизация через учётную запись Microsoft, то есть вход с лицензионной копией игры. Это та же цепочка, что и в официальном лаунчере: OAuth2 → Xbox Live → XSTS → Minecraft Services → проверка лицензии.

Результат отдаётся тем же AuthResult, что и AuthService для Ely.by и офлайна, поэтому запуск игры дальше идёт по общему пути и ничего не знает о способе входа.

Класс не показывает окно входа сам: страницу Microsoft открывает MicrosoftLoginDialog на стороне QML, а сервис даёт ему адрес страницы и разбирает адрес возврата.

Место в проекте и зависимости

Подключает authservice.h — ради общей структуры AuthResult. Экземпляр создаётся и принадлежит LauncherBackend.

Требования сборки: Qt6::Core (QJsonObject, QString, QUrl) и Qt6::Network (QNetworkAccessManager).

Сам класс собирается всегда и не зависит от Qt WebEngine — от наличия WebEngine зависит только окно, в котором показывается страница входа. Поэтому в сборке без WebEngine сервис существует, но воспользоваться им нельзя: показать страницу нечем.

Иерархия и роль

Наследует QObject: мета-объектная система, сигнал progress, владение по родителю. Виртуальных методов базового класса не переопределяет.

Псевдонимы типов

MsaAuthService::Callbackstd::function<void(const AuthResult &)>. Колбэк вызывается ровно один раз и в потоке GUI.

Публичные методы

explicit MsaAuthService(QObject *parent = nullptr)

Создаёт сервис и его QNetworkAccessManager. Конструктор помечен explicit.

static QString clientId()

Идентификатор приложения лаунчера в Microsoft. Игра ждёт его в ${clientid}: официальный лаунчер подставляет туда именно идентификатор приложения, а не случайный токен сессии.

static QUrl authorizationUrl()

Адрес страницы входа для встроенного окна браузера. В адресе уже собраны идентификатор клиента, redirect_uri и параметр выбора аккаунта — вызывающему коду достаточно открыть эту ссылку.

static bool matchRedirect(const QUrl &url, QString *code, QString *error)

Отличает адрес, на который Microsoft возвращает управление после входа, от остальной навигации внутри окна. Возвращает true только для адреса возврата.

При совпадении заполняется ровно одно из двух: code — код авторизации при успешном входе, либо error — текст отказа. Разбор адреса живёт здесь, а не в QML, потому что правила совпадения обязаны совпадать с теми, по которым сервис сам строит redirect_uri.

void loginWithCode(const QString &code, Callback callback)

Полный вход по коду, полученному из окна браузера. Проходит всю цепочку: обмен кода на токен Microsoft, аутентификация в Xbox Live, авторизация XSTS, вход в Minecraft Services, проверка лицензии и получение профиля.

По ходу испускает progress с описанием текущего шага — цепочка длинная, и без обратной связи вход выглядел бы зависанием. Результат приходит в callback один раз.

Если вход прошёл, но копии игры на аккаунте нет, в результате выставлен licenseMissing: этот случай чинится только покупкой, поэтому обрабатывается отдельно от прочих ошибок.

void loginWithRefreshToken(const QString &refreshToken, Callback callback)

Продление сессии без участия пользователя. Refresh-токен Microsoft живёт куда дольше суточного токена Minecraft, так что при повторном запуске лаунчера обычно хватает его, и окно входа показывать не приходится.

Проходит ту же цепочку, начиная с обмена refresh-токена. Неудача означает, что токен окончательно протух и нужен полноценный вход через окно.

Сигналы

progress(const QString &message)

Описание текущего шага цепочки авторизации.

Обработчик показывает сообщение пользователю. Сигнал особенно важен для этого класса: шагов пять, каждый — отдельный сетевой запрос, и между ними проходит заметное время.

Владение и время жизни

Класс наследует QObject и принимает parent — родитель его и удалит. QNetworkAccessManager создаётся в конструкторе с сервисом в роли родителя.

Каждый шаг цепочки вызывается из колбэка предыдущего, поэтому незавершённый вход держит цепочку захваченных колбэков до своего конца. Уничтожение сервиса посреди цепочки обрывает её вместе с менеджером сети, и колбэк не вызывается.

Потокобезопасность

Только поток GUI. Все методы асинхронные, колбэки и сигналы приходят в поток, где создан сервис.

Взаимодействие с другими классами

LauncherBackend создаёт сервис, отдаёт в QML адрес страницы входа, принимает от окна код авторизации и вызывает loginWithCode(). Полученные refreshToken и expiresAt он сохраняет в профиле, чтобы при следующем запуске обойтись loginWithRefreshToken().

Заполненный AuthResult дальше раскладывается по полям LaunchOptions для GameLauncher — ровно так же, как результат от AuthService.

Со стороны QML вход выглядит так: LauncherBackend испускает сигнал с адресом страницы, главное окно открывает MicrosoftLoginDialog, тот следит за навигацией и возвращает код обратно в бэкенд.

Внешнее взаимодействие

Сеть, исходящие запросы. Класс последовательно обращается к пяти внешним службам: конечной точке OAuth2 Microsoft, Xbox Live, XSTS, Minecraft Services и профильной конечной точке Minecraft. Формат — JSON поверх HTTPS, кроме первого шага, где тело запроса отправляется как форма (postForm()); дальше используются postJson() и getJson() с токеном в заголовке авторизации.

Все запросы инициирует лаунчер. Ответ каждого шага разбирается тремя исходами: успех, ошибка с кодом состояния и транспортная ошибка — последняя приходит отдельным параметром, чтобы отличить недоступную сеть от отказа службы.

Повторных попыток класс не делает. Все сигналы и колбэки приходят в поток GUI.

Пример использования

auto *msa = new MsaAuthService(this);
connect(msa, &MsaAuthService::progress, this, &Backend::showStatus);

// 1. открыть окно браузера на этом адресе
emit microsoftLoginUrlReady(MsaAuthService::authorizationUrl());

// 2. когда окно поймало адрес возврата
QString code, error;
if (MsaAuthService::matchRedirect(url, &code, &error) && !code.isEmpty()) {
    msa->loginWithCode(code, [this](const AuthResult &result) {
        if (result.licenseMissing) {
            emit loginFailed(tr("На аккаунте нет копии Minecraft"));
            return;
        }
        if (!result.ok) {
            emit loginFailed(result.error);
            return;
        }
        storeSession(result);
    });
}

При создании этого документа использовался ИИ.