- 329
- 13
АВТОРИЗАЦИЯ YGGDRASIL : ОПЫТ ИСПОЛЬЗОВАНИЯ
В качестве предисловия автор написал(а):Внимание! Далее по тексту мною приведены примеры MySQL-запросов, начинающихся с INSERT, UPDATE, SELECT. Эти три слова в теге [соde][/соde] не дают нормально отправить сообщение на форум: появляется сообщение от хостинга об ошибке. Поэтому я добавил тире после третьего знака в каждом слове, например SEL-ECT. Будьте внимательны!
Yggdrasil - это имя системы авторизации официального клиента игр от Mojang (Scrolls и Minecraft). Согласно протоколу, каждый пользователь имеет следующий набор данных:
Код:
E-mail - почта, указанная при регистрации
Password - пароль от аккаунта
UUID - уникальный номер пользователя
Nickname - текущий ник пользователя
Теперь разберем то, как клиент игры подключается к серверу. Если говорить образно, в обмене данными участвуют 4 лица: Лаунчер, Клиент, Сервер, Сайт. Происходит это по следующей схеме:
- Лаунчер спрашивает у пользователя логин и пароль, а затем отправляет их на Сайт;
- Сайт проверяет правильность введенных данных и отправляет обратно Лаунчеру : Ник игрока, UUID, accessToken ;
- Лаунчер запускает Клиент игры с параметрами, полученными с предыдущего пункта;
- Игрок в Клиенте выбирает Сервер и нажимает Подключиться;
- Клиент знакомится с Сервером. Сервер отдает Клиенту ServerID - уникальный номер сервера для подключения. Клиент отдает Серверу свой ник (username);
- Клиент запрашивает разрешение у Сайта авторизации, отдавая ему свой accessToken, UUID и ServerID;
- Сайт проверяет правильность данных и если все ОК, то запоминает ServerID;
- Клиент получил разрешение от Сайта и посылает на Сервер запрос на подключение;
- Сервер, чтобы впустить Клиента спрашивает у Сайта авторизации его данные, отдавая ему Ник игрока и свой ServerID;
- Сайт передаёт Серверу информацию о параметрах игрока, чем разрешает тому войти;
- Клиент успешно заходит на сервер.
Чтобы перенастроить систему авторизации под свои нужды, мы должны изменить адреса для запросов с серверов Mojang на свой, написать скрипты для обработки этой запросов Лаунчера, Клиента и Сервера, а также организовать базу данных для хранения информации о пользователях.
Нам понадобятся:
- Клиент Minecraft с интегрированным Forge;
- Сервер Minecraft с поддержкой Forge;
- Web-сервер с PHP и MySQL;
- Редактор InClassTranslator;
- Базовые знания PHP \ SQL;
- Базовые знания о работе web-сайтов;
ИЗМЕНЕНИЕ ФАЙЛОВ ОФИЦИАЛЬНОЙ АВТОРИЗАЦИИ
Для начала нам необходимо изменить путь до сайта, на котором клиент и сервер запрашивают всю необходимую информацию о друг-друге. Откройте свой клиент Minecraft и в папке libraries найдите файл authlib-X.X.XX.jar, где X - версия библиотеки для вашего клиента (например, в Minecraft версии 1.8 используется библиотека authlib-1.5.21.jar).
Откройте этот jar-архив, найдите в нем следующий класс и извлеките его в отдельную папку:
Код:
com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService.class
Теперь запускайте InClassTranslator и открывайте YggdrasilMinecraftSessionService.class . Найдите следующие строки:
Код:
https://sessionserver.mojang.com/session/minecraft/join // скрипт, обрабатывающий запросы клиента
https://sessionserver.mojang.com/session/minecraft/hasJoined // скрипт, обрабатывающий запросы сервера
Код:
http://127.0.0.1/minecraft/auth/join.php
http://127.0.0.1/minecraft/auth/hasJoined.php
Замените в обоих местах этот класс. В результате и клиент, и сервер готовы общаться с нашими локальными скриптами авторизации.
СОЗДАНИЕ БАЗЫ ДАННЫХ
Вначале нам необходимо создать базу, в которой будут храниться следующие параметры:
Код:
id // Уникальный номер записи для базы
username // Имя пользователя
password // MD5(Пароль пользователя)
uuid // MD5(Имя пользователя)
accessToken // Уникальный номер текущей сессии пользователя
serverID // Уникальный номер сервера для этого пользователя
База будет храниться в MySQL, потому как быстрая, популярная и сетевая. Вот код для импорта:
Код:
--
-- Структура таблицы `players`
--
CREATE TABLE IF NOT EXISTS `players` (
`id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(16) NOT NULL DEFAULT '',
`password` char(32) NOT NULL DEFAULT '',
`uuid` char(32) NOT NULL DEFAULT '',
`accessToken` char(32) NOT NULL DEFAULT '',
`serverID` varchar(42) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
Код:
INS-ERT INTO 'players' ('username', 'password', 'uuid') VALUES('TaoGunner', '5f4dcc3b5aa765d61d8327deb882cf99', '263a7d63a4726905cbace067a7c84a71');
СКРИПТ #1 : ОБРАБОТКА ЗАПРОСОВ ЛАУНЧЕРА
Первый скрипт должен помочь Лаунчеру определиться с параметрами запуска Клиента. Схема работы следующая:
- Лаунчер отдаёт Сайту параметры : Логин, Пароль ;
- Сайт проверяет параметры, и если все ОК, отдает следующее : username, UUID, accessToken ;
- Лаунчер запускает Клиент, используя полученные параметры;
Содержимое скрипта launcher.php
Код:
// Проверяем, что нам передали POST-запрос
if (($_SERVER['REQUEST_METHOD'] == 'POST') && (stripos($_SERVER["CONTENT_TYPE"], "application/x-www-form-urlencoded") === 0))
{
// Проверяем, что нам передали параметр 'method' равный 'auth'
if (isset($_POST['method']) && ($_POST['method'] == 'auth'))
{
if (isset($_POST['username']) && isset($_POST['password']) && preg_match("/^[a-zA-Z0-9_-]+$/", $_POST['username']) && preg_match("/^[a-zA-Z0-9_-]+$/", $_POST['password']))
{
// Подключаемся к серверу MySQL и делаем выборку по тем данным, которые передал нам лаунчер
$mysql_link = mysql_connect($mysql_hostname,$mysql_username,$mysql_password) or die('ERROR:Проблема с MYSQL : ' . mysql_error($mysql_link));
$mysql_db = mysql_select_db($mysql_database, $mysql_link) or die('ERROR:Проблема с MYSQL : ' . mysql_error($mysql_link));
$mysql_query = 'SEL-ECT * FROM players WHERE username="'.$_POST['username'].'" AND password="'.md5($_POST['password']).'" AND uuid="'.md5($_POST['username']).'"';
$mysql_result = mysql_query($mysql_query) or die('ERROR:Проблема с MYSQL : ' . mysql_error($mysql_link));
// Если такая запись была найдена в базе - то заносим accessToken в базу и отдаём лаунчеру результат
// в виде строки OK:имя_пользователя:uuid:accessToken
if (mysql_num_rows($mysql_result) == 1)
{
$accessToken = generateAccessToken();
$mysql_query = 'UPD-ATE players SET accessToken="'.$accessToken.'" WHERE username="'.$_POST['username'].'" AND password="'.md5($_POST['password']).'" AND uuid="'.md5($_POST['username']).'"';
$mysql_result = mysql_query($mysql_query) or die('ERROR:Проблема с MYSQL : ' . mysql_error($mysql_link));
mysql_close($mysql_link);
die('OK:'.$_POST['username'].':'.md5($_POST['username']).':'.$accessToken);
}
else
{
mysql_close($mysql_link);
die ('ERROR:Неправильная связка логин \ пароль!');
}
}
die ('ERROR:Неверные параметры для авторизации!');
}
die ('ERROR:Не передан параметр-команда!');
}
die ('ERROR:POST-запрос не получен!');
// Функция, создающая accessToken при успешной попытке авторизации
function generateAccessToken()
{
srand(time());
$randNum = rand(1000000000, 2147483647) . rand(1000000000, 2147483647) . rand(0, 9);
return md5($randNum);
}
- Проверяет, что к нему обратились с POST-запросом;
- Проверяет, что в запросе есть параметр method=auth;
- Проверяет наличие и содержимое параметров username и password;
- Подключается к базе данных и проверяет наличие логина:пароля в базе;
- С помощью функции generateAccessToken() создает новый accessToken и записывает его в базу;
- Отдаёт лаунчеру ответ в формате:
Код:
OK:username:UUID:accessToken
Любопытный читатель написал(а):Зачем нужен параметр method? И почему он должен быть равен auth?
Ответ : дело в том, что скрипт launcher.php рассчитан не только на авторизацию, но и на многие другие вещи, которые я целенаправленно вырезал. Например, если в полном скрипте передать method=register, то скрипт зарегистрирует нового пользователя, а если method=skin, загрузит скин пользователя на сервер. Это всё не имеет к теме прямого отношения, посему вырезано.
После получения этих данных у Лаунчера есть все параметры для запуска Клиента Minecraft.
СКРИПТ #2 : ОБРАБОТКА ЗАПРОСОВ КЛИЕНТА
Итак, Клиент запущен с параметрами --username , --UUID и --accessToken и хочет подключиться к Серверу, посылая их общему другу и связующему - Сайту, вот такое сообщение в формате JSON:
Код:
{
"accessToken": "<accessToken>",
"selectedProfile": "<UUID>",
"serverId": "<serverID>"
}
Теперь Сайту необходим скрипт, чтобы понять, на какой сервер и какой клиент хочет подключиться:
Содержимое скрипта join.php
Код:
// Проверяем, что мы получили POST-запрос с JSON-содержимым
if (($_SERVER['REQUEST_METHOD'] == 'POST') && (stripos($_SERVER["CONTENT_TYPE"], "application/json") === 0))
{
$data = json_decode(file_get_contents('php://input'), TRUE);
$mysql_link = mysql_connect($mysql_hostname,$mysql_username,$mysql_password);
$mysql_db = mysql_select_db($mysql_database, $mysql_link);
$mysql_query = 'SEL-ECT id FROM players WHERE uuid="'.$data['selectedProfile'].'" AND accessToken="'.$data['accessToken'].'"';
$mysql_result = mysql_query($mysql_query);
if (mysql_num_rows($mysql_result) == 1)
{
$mysql_query = 'UPD-ATE players SET serverID="'.$data['serverId'].'" WHERE uuid="'.$data['selectedProfile'].'" AND accessToken="'.$data['accessToken'].'"';
$mysql_result = mysql_query($mysql_query);
}
else
{
echo '{"error": "Auth error","errorMessage": "Ошибка! Перезайдите через лаунчер!","cause": "Auth error"}';
}
mysql_close($mysql_link);
}
- Проверяет, что к нему обратились с POST-запросом и что этот запрос в формате JSON;
- Декодирует входящий поток и разбирает его на 3 параметра (описаны выше);
- Подключается к базе данных и проверяет наличие связки UUID:accessToken;
- Если находит - добавляет serverId в базу данных. Клиенту уже ничего не отдаёт (пустую страницу);
- Если не находит - отдаёт ответ об ошибке в формате JSON:
Код:
{
"error": "Короткое описание ошибки",
"errorMessage": "Подробное описание, ОТОБРАЖАЕМОЕ В КЛИЕНТЕ!",
"cause": "Причина ошибки (опционально)"
}
СКРИПТ #3 : ОБРАБОТКА ЗАПРОСОВ СЕРВЕРА
Теперь Клиент стучится на Сервер с просьбой войти. Серверу необходимо удостоверится, что Клиент прошел процедуру авторизации и их общий друг, Сайт, знает его. Для этого Сервер отправляет на Сайт GET-запрос:
Код:
../hasJoined?username=имя_пользователя&serverId=свой_сервер_id
Содержимое скрипта hasJoined.php
Код:
// Ниже - ссылка, где хранятся скины игроков
$skin_url = 'http://skins.minecraft.net/MinecraftSkins/';
// Если скрипт получил от сервера Minecraft корректный GET-запрос
if (isset($_GET['username']) && isset($_GET['serverId']) && strlen($_GET['serverId']) >= 40)
{
// И если эти параметры состоят не из запрещенных символов
if (preg_match("/^[a-zA-Z0-9_-]+$/", $_GET['username']) && preg_match("/^[a-zA-Z0-9_-]+$/", $_GET['serverId']))
{
$mysql_link = mysql_connect($mysql_hostname,$mysql_username,$mysql_password);
$mysql_db = mysql_select_db($mysql_database, $mysql_link);
$mysql_query = 'SEL-ECT * FROM players WHERE username="'.$_GET['username'].'" AND serverId="'.$_GET['serverId'].'"';
$mysql_result = mysql_query($mysql_query);
// Если в базе данных есть запись, то отдаем серверу информацию о пользователе
if (mysql_num_rows($mysql_result) == 1)
{
$mysql_string = mysql_fetch_row($mysql_result);
$time = time();
$base64 = '{"timestamp":'.$time.'","profileId":"'.$mysql_string[3].'","profileName":"'.$mysql_string[1].'","textures":{"SKIN":{"url":"'.$skin_url.$mysql_string[1].'.png"}}}';
echo '{"id":"'.$mysql_string[3].'","name":"'.$mysql_string[1].'","properties":[{"name":"textures","value":"'.base64_encode($base64).'"}]}';
}
mysql_close($mysql_link);
}
}
- Проверяет, что к нему обратились с GET-запросом и что запрос не содержит запрещенных символов;
- Подключается к базе данных и проверяет наличие связки username:serverId;
- Если такая запись есть в базе - выдаёт ответ в формате JSON:
Код:
{
"id": "<UUID>",
"name": "<username>",
"properties": [
{
"name": "textures",
"value": "Длинная Base64-строка (см.ниже)"
}
]
}
Код:
{
"timestamp": временная_метка,
"profileId": "<UUID>",
"profileName": "<username>",
"textures": {
"SKIN": {
"url": "http://ссылка/на/скин/игрока.png"
}
}
}
ПОДВЕДЕМ ИТОГИ
Что мы получили? :
- Вы узнали немного о Yggdrasil - официальном протоколе авторизации Mojang;
- Вы научились использовать протокол в своих целях;
- Теперь у вас есть потенциал к дальнейшему обучению и самостоятельному исследованию. Поздравляю.
Что еще можно сделать? :
- Написать собственный лаунчер, с авторизацией, скинами и автоматическим подключением;
- Организовать систему банов и выдачу уникальных сообщений при подключении;
- Дописать защиту от брутфорса с задержкой между авторизациями;
- И, наконец, сделать авторизацию прямо в клиенте, ибо лаунчер - для детей и слабаков.