172 lines
11 KiB
Markdown
172 lines
11 KiB
Markdown
|
|
# AuthService
|
|||
|
|
|
|||
|
|
## Обзор класса
|
|||
|
|
|
|||
|
|
`Minecraft_launcher` поддерживает три способа входа: офлайн-профиль без пароля, учётную запись
|
|||
|
|
Ely.by и учётную запись Microsoft. `AuthService` закрывает первые два: это Yggdrasil-клиент Ely.by
|
|||
|
|
плюс офлайн-режим. За третий отвечает [MsaAuthService](MsaAuthService.md).
|
|||
|
|
|
|||
|
|
Результат любого способа — структура `AuthResult`, объявленная в этом же заголовке. Она содержит
|
|||
|
|
ровно то, что подставляется в аргументы запуска вида `${auth_*}`, поэтому дальше запуск игры идёт
|
|||
|
|
по общему пути независимо от того, как пользователь вошёл.
|
|||
|
|
|
|||
|
|
Класс также умеет скачивать `authlib-injector` — библиотеку, которая перенаправляет обращения игры
|
|||
|
|
к серверу авторизации на Ely.by.
|
|||
|
|
|
|||
|
|
## Место в проекте и зависимости
|
|||
|
|
|
|||
|
|
Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md); других владельцев нет.
|
|||
|
|
Заголовок подключает [MsaAuthService](MsaAuthService.md) — ради общей структуры `AuthResult` — и
|
|||
|
|
[GameLauncher](GameLauncher.md) через поля `LaunchOptions`, которые заполняются из результата
|
|||
|
|
авторизации.
|
|||
|
|
|
|||
|
|
Требования сборки: `Qt6::Core` (`QDateTime`, `QJsonObject`, `QString`) и `Qt6::Network`
|
|||
|
|
(`QNetworkAccessManager`).
|
|||
|
|
|
|||
|
|
## Иерархия и роль
|
|||
|
|
|
|||
|
|
Наследует `QObject`: даёт мета-объектную систему, сигнал `progress` и владение по родителю.
|
|||
|
|
Виртуальных методов базового класса не переопределяет.
|
|||
|
|
|
|||
|
|
## Публичные структуры
|
|||
|
|
|
|||
|
|
### AuthResult
|
|||
|
|
|
|||
|
|
Результат авторизации — то, что подставляется в `${auth_*}` аргументы запуска.
|
|||
|
|
|
|||
|
|
| Поле | Тип | Описание |
|
|||
|
|
|------|-----|----------|
|
|||
|
|
| `ok` | `bool` | Авторизация удалась |
|
|||
|
|
| `twoFactorRequired` | `bool` | Ely.by отклонил пароль с пометкой two factor — нужен одноразовый код |
|
|||
|
|
| `licenseMissing` | `bool` | Вход в Microsoft прошёл, но копии игры на аккаунте нет. Обрабатывается отдельно от прочих ошибок, потому что чинится только покупкой |
|
|||
|
|
| `error` | `QString` | Текст ошибки, когда `ok` равен `false` |
|
|||
|
|
| `playerName` | `QString` | Подставляется в `${auth_player_name}` |
|
|||
|
|
| `uuid` | `QString` | Подставляется в `${auth_uuid}`; hex без дефисов |
|
|||
|
|
| `accessToken` | `QString` | Подставляется в `${auth_access_token}` |
|
|||
|
|
| `clientToken` | `QString` | Подставляется в `${clientid}` |
|
|||
|
|
| `userType` | `QString` | Подставляется в `${user_type}`; принимает значения `legacy` (офлайн), `msa` (Microsoft) и `ELYBY` |
|
|||
|
|
| `refreshToken` | `QString` | Только для аккаунтов Microsoft: продлевает сессию без ввода пароля |
|
|||
|
|
| `xuid` | `QString` | Только для Microsoft; подставляется в `${auth_xuid}` |
|
|||
|
|
| `expiresAt` | `QDateTime` | Только для Microsoft: UTC-время, когда протухает `accessToken` |
|
|||
|
|
|
|||
|
|
## Псевдонимы типов
|
|||
|
|
|
|||
|
|
`AuthService::Callback` — `std::function<void(const AuthResult &)>`. Все сетевые методы
|
|||
|
|
асинхронные: колбэк вызывается ровно один раз и всегда в потоке GUI.
|
|||
|
|
|
|||
|
|
## Публичные методы
|
|||
|
|
|
|||
|
|
#### explicit AuthService(QObject \*parent = nullptr)
|
|||
|
|
|
|||
|
|
Создаёт сервис и его `QNetworkAccessManager`. Конструктор помечен `explicit`.
|
|||
|
|
|
|||
|
|
#### static AuthResult offline(const QString &nickname)
|
|||
|
|
|
|||
|
|
Готовит результат для офлайн-профиля без единого сетевого запроса. UUID выводится из ника так же,
|
|||
|
|
как это делает сам Minecraft в офлайне: `UUID.nameUUIDFromBytes(("OfflinePlayer:" + name)
|
|||
|
|
.getBytes(UTF_8))`. Благодаря этому один и тот же ник всегда даёт один и тот же UUID, и прогресс
|
|||
|
|
на сервере не теряется.
|
|||
|
|
|
|||
|
|
#### static QString generateClientToken()
|
|||
|
|
|
|||
|
|
Случайный `clientToken` лаунчера. Генерируется один раз на профиль и хранится вместе с ним:
|
|||
|
|
Yggdrasil связывает выданный `accessToken` именно с этим значением.
|
|||
|
|
|
|||
|
|
#### void loginElyBy(const QString &login, const QString &password, const QString &clientToken, const QString &accessToken, Callback callback)
|
|||
|
|
|
|||
|
|
Полный цикл входа в Ely.by: сначала проверка имеющегося токена, затем его продление, и только при
|
|||
|
|
неудаче — авторизация по паролю. Пароль можно оставить пустым, если уже есть рабочий
|
|||
|
|
`accessToken`, — тогда пользователю не придётся вводить его заново.
|
|||
|
|
|
|||
|
|
По ходу работы испускает `progress` с описанием текущего шага. Результат приходит в `callback`
|
|||
|
|
один раз; при ответе с пометкой двухфакторной аутентификации в нём выставлен
|
|||
|
|
`twoFactorRequired`, и вызывающий код должен спросить у пользователя код и продолжить через
|
|||
|
|
`loginElyByWithTotp()`.
|
|||
|
|
|
|||
|
|
#### void loginElyByWithTotp(const QString &login, const QString &password, const QString &totp, const QString &clientToken, Callback callback)
|
|||
|
|
|
|||
|
|
Повтор авторизации с одноразовым кодом двухфакторной аутентификации. Пароль и код объединяются в
|
|||
|
|
одно поле в формате «пароль:код», как того требует Ely.by.
|
|||
|
|
|
|||
|
|
#### void ensureAuthlibInjector(const QString &targetDir, std::function<void(const QString &path, const QString &error)> callback)
|
|||
|
|
|
|||
|
|
Скачивает `authlib-injector` в `targetDir`, если его там ещё нет. Колбэк получает либо путь к
|
|||
|
|
готовому jar, либо текст ошибки — заполнено всегда ровно одно из двух.
|
|||
|
|
|
|||
|
|
Библиотека нужна только для профилей Ely.by: она подключается к JVM аргументом `-javaagent` и
|
|||
|
|
перенаправляет обращения игры к серверу авторизации.
|
|||
|
|
|
|||
|
|
## Сигналы
|
|||
|
|
|
|||
|
|
#### progress(const QString &message)
|
|||
|
|
|
|||
|
|
Описание текущего шага авторизации. Испускается по ходу всех сетевых операций.
|
|||
|
|
|
|||
|
|
Обработчик показывает сообщение пользователю: в главном окне лаунчера оно попадает в плашку
|
|||
|
|
статуса и держится до следующего сообщения, потому что шаг может занять заметное время.
|
|||
|
|
|
|||
|
|
## Владение и время жизни
|
|||
|
|
|
|||
|
|
Класс наследует `QObject` и принимает `parent` в конструкторе — родитель его и удалит.
|
|||
|
|
`QNetworkAccessManager` создаётся в конструкторе с сервисом в роли родителя и уничтожается вместе
|
|||
|
|
с ним.
|
|||
|
|
|
|||
|
|
Колбэки захватываются по значению и живут до своего единственного вызова. Уничтожение сервиса во
|
|||
|
|
время незавершённого запроса отменяет запрос вместе с менеджером сети — колбэк в этом случае не
|
|||
|
|
вызывается, поэтому захватывать в него сырые указатели на объекты с меньшим временем жизни, чем у
|
|||
|
|
сервиса, нельзя.
|
|||
|
|
|
|||
|
|
## Потокобезопасность
|
|||
|
|
|
|||
|
|
Только поток GUI. Все сетевые методы асинхронные, и их колбэки вызываются в том же потоке, в
|
|||
|
|
котором создан сервис. Собственной синхронизации в классе нет.
|
|||
|
|
|
|||
|
|
## Взаимодействие с другими классами
|
|||
|
|
|
|||
|
|
`LauncherBackend` вызывает методы входа при запуске игры и переправляет сигнал `progress` в
|
|||
|
|
интерфейс. Полученный `AuthResult` он раскладывает по полям `LaunchOptions`, которые уходят в
|
|||
|
|
[GameLauncher](GameLauncher.md). Путь, возвращённый `ensureAuthlibInjector()`, попадает в поле
|
|||
|
|
`authlibInjectorPath` тех же параметров запуска.
|
|||
|
|
|
|||
|
|
Сохранением токенов между запусками занимается `LauncherBackend`: сам сервис ничего не пишет на
|
|||
|
|
диск, кроме скачанного jar.
|
|||
|
|
|
|||
|
|
## Внешнее взаимодействие
|
|||
|
|
|
|||
|
|
**Сеть, исходящие запросы.** Класс общается с сервером авторизации Ely.by через
|
|||
|
|
`QNetworkAccessManager`. Формат — JSON поверх HTTPS, запросы инициирует всегда лаунчер. Внутренний
|
|||
|
|
помощник `postJson()` разделяет три исхода: успешный ответ, ответ с кодом ошибки и транспортную
|
|||
|
|
ошибку — последняя отдаётся отдельным параметром, чтобы отличить недоступную сеть от отказа
|
|||
|
|
сервера.
|
|||
|
|
|
|||
|
|
Отдельным каналом идёт загрузка `authlib-injector` — обычная HTTPS-загрузка файла в
|
|||
|
|
`targetDir`. Повторных попыток при неудаче класс не делает: решение о повторе принимает вызывающий
|
|||
|
|
код.
|
|||
|
|
|
|||
|
|
Все сигналы и колбэки приходят в поток GUI.
|
|||
|
|
|
|||
|
|
## Пример использования
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
auto *auth = new AuthService(this);
|
|||
|
|
connect(auth, &AuthService::progress, this, &Backend::showStatus);
|
|||
|
|
|
|||
|
|
auth->loginElyBy(profile.login, profile.password,
|
|||
|
|
profile.clientToken, profile.accessToken,
|
|||
|
|
[this](const AuthResult &result) {
|
|||
|
|
if (result.twoFactorRequired) {
|
|||
|
|
emit twoFactorRequired(m_pendingProfileName);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!result.ok) {
|
|||
|
|
emit launchError(result.error);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
continueLaunch(result);
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
При создании этого документа использовался ИИ.
|