1 parent
1943996608
commit
f10b654953
34 files changed
+2099
-4576
No files matched your search
+57
-137
@@ -1,160 +1,80 @@
|
||||
# MsaAuthService
|
||||
|
||||
## Обзор класса
|
||||
`msaauthservice.h`, `msaauthservice.cpp`
|
||||
|
||||
`MsaAuthService` — авторизация через учётную запись Microsoft, то есть вход с лицензионной копией
|
||||
игры. Это та же цепочка, что и в официальном лаунчере: OAuth2 → Xbox Live → XSTS → Minecraft
|
||||
Services → проверка лицензии.
|
||||
Вход через аккаунт Microsoft, то есть с лицензионной копией игры. Цепочка такая же, как у
|
||||
официального лаунчера: OAuth2 → Xbox Live → XSTS → Minecraft Services → проверка лицензии →
|
||||
профиль. Результат приходит в `AuthResult` из [AuthService](AuthService.md).
|
||||
|
||||
Результат отдаётся тем же `AuthResult`, что и [AuthService](AuthService.md) для Ely.by и офлайна,
|
||||
поэтому запуск игры дальше идёт по общему пути и ничего не знает о способе входа.
|
||||
Окно входа сервис не показывает. Страницу Microsoft открывает
|
||||
[MicrosoftLoginDialog](../qml/MicrosoftLoginDialog.md), а сервис даёт адрес этой страницы и умеет
|
||||
узнать адрес возврата. Класс компилируется всегда, но без Qt WebEngine показать страницу нечем, и
|
||||
[LauncherBackend](LauncherBackend.md) сразу отвечает ошибкой.
|
||||
|
||||
Класс не показывает окно входа сам: страницу Microsoft открывает
|
||||
[MicrosoftLoginDialog](../qml/MicrosoftLoginDialog.md) на стороне QML, а сервис даёт ему адрес
|
||||
страницы и разбирает адрес возврата.
|
||||
## Чьё это приложение
|
||||
|
||||
## Место в проекте и зависимости
|
||||
Сейчас используется `client_id` официального лаунчера Mojang (`00000000402b5328`). Это старое
|
||||
приложение Live Connect, поэтому и адреса от `login.live.com`:
|
||||
|
||||
Подключает `authservice.h` — ради общей структуры `AuthResult`. Экземпляр создаётся и принадлежит
|
||||
[LauncherBackend](LauncherBackend.md).
|
||||
| Константа | Значение |
|
||||
|-----------|----------|
|
||||
| `kAuthorizeUrl` | `https://login.live.com/oauth20_authorize.srf` |
|
||||
| `kTokenUrl` | `https://login.live.com/oauth20_token.srf` |
|
||||
| `kRedirectUri` | `https://login.live.com/oauth20_desktop.srf` |
|
||||
| `kScope` | `service::user.auth.xboxlive.com::MBI_SSL` |
|
||||
| `kRpsTicketPrefix` | пусто |
|
||||
|
||||
Требования сборки: `Qt6::Core` (`QJsonObject`, `QString`, `QUrl`) и `Qt6::Network`
|
||||
(`QNetworkAccessManager`).
|
||||
Это временное решение. Чтобы перейти на своё приложение в Azure (с включённым «Allow public client
|
||||
flows»), меняются эти шесть констант. Что именно подставить, написано в комментарии в начале
|
||||
`msaauthservice.cpp`. Главное, что легко пропустить: у нового приложения в `RpsTicket` нужна
|
||||
приставка `d=`.
|
||||
|
||||
Сам класс собирается всегда и не зависит от Qt WebEngine — от наличия WebEngine зависит только
|
||||
окно, в котором показывается страница входа. Поэтому в сборке без WebEngine сервис существует, но
|
||||
воспользоваться им нельзя: показать страницу нечем.
|
||||
`clientId()` отдаёт этот id наружу. Бэкенд подставляет его в `${clientid}`, потому что официальный
|
||||
лаунчер передаёт туда id приложения, а не случайный токен.
|
||||
|
||||
## Иерархия и роль
|
||||
## Окно входа
|
||||
|
||||
Наследует `QObject`: мета-объектная система, сигнал `progress`, владение по родителю. Виртуальных
|
||||
методов базового класса не переопределяет.
|
||||
`authorizationUrl()` — адрес страницы входа с `client_id`, `response_type=code`, `redirect_uri`,
|
||||
`scope` и `prompt=select_account`. Последний параметр нужен, чтобы при повторном входе можно было
|
||||
выбрать другой аккаунт.
|
||||
|
||||
## Псевдонимы типов
|
||||
`matchRedirect(url, &code, &error)` возвращает `true`, только если хост и путь совпадают с
|
||||
`kRedirectUri`. Код или ошибка ищутся сначала в query, потом во фрагменте. В `error` попадает
|
||||
`error_description`, а если его нет, то `error`. Эта проверка сделана в C++, а не в QML, чтобы она
|
||||
всегда совпадала с тем адресом, который сервис сам передаёт в `redirect_uri`.
|
||||
|
||||
`MsaAuthService::Callback` — `std::function<void(const AuthResult &)>`. Колбэк вызывается ровно
|
||||
один раз и в потоке GUI.
|
||||
## Цепочка
|
||||
|
||||
## Публичные методы
|
||||
`loginWithCode(code, callback)` меняет код на токен (`grant_type=authorization_code`).
|
||||
`loginWithRefreshToken(refreshToken, callback)` делает то же по refresh-токену. Дальше путь общий:
|
||||
|
||||
#### explicit MsaAuthService(QObject \*parent = nullptr)
|
||||
1. **Токен Microsoft.** Форма на `kTokenUrl`. Из ответа берутся `access_token` и `refresh_token`.
|
||||
2. **Xbox Live.** `user.auth.xboxlive.com/user/authenticate`, `RpsTicket` = приставка + токен
|
||||
Microsoft.
|
||||
3. **XSTS.** `xsts.auth.xboxlive.com/xsts/authorize` для `rp://api.minecraftservices.com/`. При 401
|
||||
код `XErr` переводится в понятный текст: нет профиля Xbox, страна не поддерживается, нужна
|
||||
проверка возраста, детский аккаунт без разрешения взрослого. Отсюда же берутся `uhs` и `xid`
|
||||
(он станет `xuid`).
|
||||
4. **Minecraft Services.** `login_with_xbox` с `XBL3.0 x=<uhs>;<xsts>`. В ответе токен Minecraft и
|
||||
`expires_in`, обычно сутки; от него считается `expiresAt`.
|
||||
5. **Лицензия.** `entitlements/mcstore`. Результат только запоминается и сам вход не останавливает.
|
||||
6. **Профиль.** `minecraft/profile`. Ответы 404 и 401 означают, что профиля нет: выставляется
|
||||
`licenseMissing`. Текст ошибки зависит от шага 5. Если игра куплена, значит, игрок ещё не выбрал
|
||||
ник. Если не куплена, значит, лицензии нет.
|
||||
|
||||
Создаёт сервис и его `QNetworkAccessManager`. Конструктор помечен `explicit`.
|
||||
`userType` = `msa`, `clientToken` = id приложения.
|
||||
|
||||
#### static QString clientId()
|
||||
Каждый шаг испускает `progress(message)` до отправки запроса. Таймаут на каждый запрос 20 секунд,
|
||||
повторных попыток нет. Сетевая ошибка и отказ сервера дают разные тексты.
|
||||
|
||||
Идентификатор приложения лаунчера в Microsoft. Игра ждёт его в `${clientid}`: официальный лаунчер
|
||||
подставляет туда именно идентификатор приложения, а не случайный токен сессии.
|
||||
Refresh-токен Microsoft живёт намного дольше суточного токена Minecraft, поэтому при обычном
|
||||
запуске окно входа не нужно: бэкенд вызывает `loginWithRefreshToken()`. Если Microsoft не вернул
|
||||
новый refresh-токен, бэкенд оставляет старый.
|
||||
|
||||
#### 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](GameLauncher.md) — ровно так же, как результат от [AuthService](AuthService.md).
|
||||
|
||||
Со стороны QML вход выглядит так: `LauncherBackend` испускает сигнал с адресом страницы, главное
|
||||
окно открывает [MicrosoftLoginDialog](../qml/MicrosoftLoginDialog.md), тот следит за навигацией и
|
||||
возвращает код обратно в бэкенд.
|
||||
|
||||
## Внешнее взаимодействие
|
||||
|
||||
**Сеть, исходящие запросы.** Класс последовательно обращается к пяти внешним службам: конечной
|
||||
точке OAuth2 Microsoft, Xbox Live, XSTS, Minecraft Services и профильной конечной точке Minecraft.
|
||||
Формат — JSON поверх HTTPS, кроме первого шага, где тело запроса отправляется как форма
|
||||
(`postForm()`); дальше используются `postJson()` и `getJson()` с токеном в заголовке
|
||||
авторизации.
|
||||
|
||||
Все запросы инициирует лаунчер. Ответ каждого шага разбирается тремя исходами: успех, ошибка с
|
||||
кодом состояния и транспортная ошибка — последняя приходит отдельным параметром, чтобы отличить
|
||||
недоступную сеть от отказа службы.
|
||||
|
||||
Повторных попыток класс не делает. Все сигналы и колбэки приходят в поток GUI.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
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);
|
||||
});
|
||||
}
|
||||
```
|
||||
Колбэк вызывается один раз в GUI-потоке. Шаги вызываются из колбэков предыдущих. Если удалить
|
||||
сервис посреди цепочки, она оборвётся вместе с менеджером сети, и колбэк не вызовется.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user