[Гайд][PHP][SQL] Авторизация Yggdrasil на домашнем сервере

329
13
АВТОРИЗАЦИЯ YGGDRASIL : ОПЫТ ИСПОЛЬЗОВАНИЯ​

В качестве предисловия автор написал(а):
Внимание! Далее по тексту мною приведены примеры MySQL-запросов, начинающихся с INSERT, UPDATE, SELECT. Эти три слова в теге [соde][/соde] не дают нормально отправить сообщение на форум: появляется сообщение от хостинга об ошибке. Поэтому я добавил тире после третьего знака в каждом слове, например SEL-ECT. Будьте внимательны!

Yggdrasil - это имя системы авторизации официального клиента игр от Mojang (Scrolls и Minecraft). Согласно протоколу, каждый пользователь имеет следующий набор данных:
Код:
E-mail          - почта, указанная при регистрации
Password        - пароль от аккаунта
UUID            - уникальный номер пользователя
Nickname        - текущий ник пользователя
Обратим внимание на Nickname. Я не зря написал, что он текущий: его можно менять, о чем Mojang объявили с выходом Minecraft версии 1.7.2 . А на сервере вся информация о приватах, вещах и состоянии персонажа привязана к UUID, который никогда не меняется. Запомнили? Едем дальше...

Теперь разберем то, как клиент игры подключается к серверу. Если говорить образно, в обмене данными участвуют 4 лица: Лаунчер, Клиент, Сервер, Сайт. Происходит это по следующей схеме:
  1. Лаунчер спрашивает у пользователя логин и пароль, а затем отправляет их на Сайт;
  2. Сайт проверяет правильность введенных данных и отправляет обратно Лаунчеру : Ник игрока, UUID, accessToken ;
  3. Лаунчер запускает Клиент игры с параметрами, полученными с предыдущего пункта;
  4. Игрок в Клиенте выбирает Сервер и нажимает Подключиться;
  5. Клиент знакомится с Сервером. Сервер отдает Клиенту ServerID - уникальный номер сервера для подключения. Клиент отдает Серверу свой ник (username);
  6. Клиент запрашивает разрешение у Сайта авторизации, отдавая ему свой accessToken, UUID и ServerID;
  7. Сайт проверяет правильность данных и если все ОК, то запоминает ServerID;
  8. Клиент получил разрешение от Сайта и посылает на Сервер запрос на подключение;
  9. Сервер, чтобы впустить Клиента спрашивает у Сайта авторизации его данные, отдавая ему Ник игрока и свой ServerID;
  10. Сайт передаёт Серверу информацию о параметрах игрока, чем разрешает тому войти;
  11. Клиент успешно заходит на сервер.

Чтобы перенастроить систему авторизации под свои нужды, мы должны изменить адреса для запросов с серверов 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   //  скрипт, обрабатывающий запросы сервера
Теперь мы хотим, чтобы запросы обрабатывались нашими скриптами. Например, у меня скрипты авторизации расположены на web-сервере по адресу http://127.0.0.1/minecraft/auth/ , значит, я меняю эти строки на:
Код:
http://127.0.0.1/minecraft/auth/join.php
http://127.0.0.1/minecraft/auth/hasJoined.php
Измените строки и сохраните полученный класс. Он нам пригодится в 2 местах: в библиотеке authlib-X.X.XX.jar клиента игры и, соответственно, в minecraft_server.jar на сервере (да, он лежит внутри).
Замените в обоих местах этот класс. В результате и клиент, и сервер готовы общаться с нашими локальными скриптами авторизации.

СОЗДАНИЕ БАЗЫ ДАННЫХ​

Вначале нам необходимо создать базу, в которой будут храниться следующие параметры:
Код:
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;
И для проверки добавим одного пользователя (TaoGunner:password)

Код:
INS-ERT INTO 'players' ('username', 'password', 'uuid') VALUES('TaoGunner', '5f4dcc3b5aa765d61d8327deb882cf99', '263a7d63a4726905cbace067a7c84a71');


СКРИПТ #1 : ОБРАБОТКА ЗАПРОСОВ ЛАУНЧЕРА​

Первый скрипт должен помочь Лаунчеру определиться с параметрами запуска Клиента. Схема работы следующая:
  1. Лаунчер отдаёт Сайту параметры : Логин, Пароль ;
  2. Сайт проверяет параметры, и если все ОК, отдает следующее : username, UUID, accessToken ;
  3. Лаунчер запускает Клиент, используя полученные параметры;

Содержимое скрипта 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);
}
Что делает скрипт? :
  1. Проверяет, что к нему обратились с POST-запросом;
  2. Проверяет, что в запросе есть параметр method=auth;
  3. Проверяет наличие и содержимое параметров username и password;
  4. Подключается к базе данных и проверяет наличие логина:пароля в базе;
  5. С помощью функции generateAccessToken() создает новый accessToken и записывает его в базу;
  6. Отдаёт лаунчеру ответ в формате:
Код:
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);
}
Что делает скрипт? :
  1. Проверяет, что к нему обратились с POST-запросом и что этот запрос в формате JSON;
  2. Декодирует входящий поток и разбирает его на 3 параметра (описаны выше);
  3. Подключается к базе данных и проверяет наличие связки UUID:accessToken;
  4. Если находит - добавляет serverId в базу данных. Клиенту уже ничего не отдаёт (пустую страницу);
  5. Если не находит - отдаёт ответ об ошибке в формате JSON:
Код:
{
  "error": "Короткое описание ошибки",
  "errorMessage": "Подробное описание, ОТОБРАЖАЕМОЕ В КЛИЕНТЕ!",
  "cause": "Причина ошибки (опционально)"
}
Если после запроса Клиент ничего не получил в ответ - то он знает, что всё ОК и можно попробовать подключиться к Серверу. Но пустит ли Сервер его - мы узнаем позже...

СКРИПТ #3 : ОБРАБОТКА ЗАПРОСОВ СЕРВЕРА​

Теперь Клиент стучится на Сервер с просьбой войти. Серверу необходимо удостоверится, что Клиент прошел процедуру авторизации и их общий друг, Сайт, знает его. Для этого Сервер отправляет на Сайт GET-запрос:
Код:
../hasJoined?username=имя_пользователя&serverId=свой_сервер_id
Сайту остается только решить, пускать этого Клиента на этот Сервер, или не пускать. Если пускать, то Сайт должен передать Серверу всю информацию о Клиенте, начиная от UUID, заканчивая ссылкой на скин.

Содержимое скрипта 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);
  }
}
Что делает скрипт? :
  1. Проверяет, что к нему обратились с GET-запросом и что запрос не содержит запрещенных символов;
  2. Подключается к базе данных и проверяет наличие связки username:serverId;
  3. Если такая запись есть в базе - выдаёт ответ в формате JSON:
Код:
{
  "id": "<UUID>",
  "name": "<username>",
  "properties": [
    {
      "name": "textures",
      "value": "Длинная Base64-строка (см.ниже)"
    }
  ]
}
В строке "value" содержатся Base64-зашифрованные JSON-данные, а именно:
Код:
{
  "timestamp": временная_метка,
  "profileId": "<UUID>",
  "profileName": "<username>",
  "textures": {
    "SKIN": {
      "url": "http://ссылка/на/скин/игрока.png"
    }
  }
}
Вот пример реально существующего аккаунта Mojang, для наглядности.

ПОДВЕДЕМ ИТОГИ​

Что мы получили? :
  • Вы узнали немного о Yggdrasil - официальном протоколе авторизации Mojang;
  • Вы научились использовать протокол в своих целях;
  • Теперь у вас есть потенциал к дальнейшему обучению и самостоятельному исследованию. Поздравляю.

Что еще можно сделать? :
  • Написать собственный лаунчер, с авторизацией, скинами и автоматическим подключением;
  • Организовать систему банов и выдачу уникальных сообщений при подключении;
  • Дописать защиту от брутфорса с задержкой между авторизациями;
  • И, наконец, сделать авторизацию прямо в клиенте, ибо лаунчер - для детей и слабаков.

 
Хорошо написано, я за перенос в учебник.
Пара вопросов:
1) У тебя написано: 
Клиент запрашивает у Сервера параметр ServerID - уникальный номер для подключения;
а потом
Сервер также запрашивает разрешение у Сайта авторизации, отдавая ему Ник игрока и свой ServerID;
Откуда при таком раскладе у сервера информация об игроке, например, ник? И в какой момент сервер делает этот запрос? Я не шарю в теме, но имхо пропущен какой-то шаг. Например, после запроса от клиента к сайту клиент ещё раз коннектится к серверу уже в попытке подключиться и при этом отдает ему ник игрока.
2) Как смена ника работает с сейвами? ЕМНИП даже в 1.7 в качестве названия для сейва используется ник игрока, а не UUID. Или я путаю?
 
GloomyFolken написал(а):
Хорошо написано, я за перенос в учебник.
Пара вопросов:
1) У тебя написано: 
Клиент запрашивает у Сервера параметр ServerID - уникальный номер для подключения;
а потом
Сервер также запрашивает разрешение у Сайта авторизации, отдавая ему Ник игрока и свой ServerID;
Откуда при таком раскладе у сервера информация об игроке, например, ник? И в какой момент сервер делает этот запрос? Я не шарю в теме, но имхо пропущен какой-то шаг. Например, после запроса от клиента к сайту клиент ещё раз коннектится к серверу уже в попытке подключиться и при этом отдает ему ник игрока.
2) Как смена ника работает с сейвами? ЕМНИП даже в 1.7 в качестве названия для сейва используется ник игрока, а не UUID. Или я путаю?
  1. Не торопись, юный падаван, Сервер узнает информацию о Клиенте чуть позже. Терпение...
  2. Попробуй побегать на сервере версий 1.7.2+ и глянь просмотрщиком NBT получившееся сохранение. ЕМНИП, там уже лежат UUID вместо ников.
 
1) На восьмом шаге у сервера откуда-то уже есть ник игрока. При этом единственное взаимодействие с сервером до этого - это запрос от клиента на serverID. Откуда у сервера берется ник?
2) В НБТ UUID лежит как минимум с 1.6.4. Проблема в том, что название сейва - это ник игрока. Не будет же сервер перебирать все сейвы в поисках того, где лежит нужный UUID. Как работает смена ника? Игрок вайпается?
 
kiss_3kb.1432378729.png
 
О, дописал, клёво. А в какой момент сервер чекает авторизацию игрока? Просто ждет некоторое время после "знакомства" или ему клиент отправляет "вуаля, я договорился с сервером авторизации, чекай"?
 
GloomyFolken написал(а):
1) На восьмом шаге у сервера откуда-то уже есть ник игрока. При этом единственное взаимодействие с сервером до этого - это запрос от клиента на serverID. Откуда у сервера берется ник?
2) В НБТ UUID лежит как минимум с 1.6.4. Проблема в том, что название сейва - это ник игрока. Не будет же сервер перебирать все сейвы в поисках того, где лежит нужный UUID. Как работает смена ника? Игрок вайпается?
  1. Хорошее замечание. Добавил объяснение в гайд. Клиент получает ServerId, а Сервер - username . По сути - обычное рукопожатие.
  2. У меня на сервере, например, в папке playerdata лежит файл 263a7d63-a472-6905-cbac-e067a7c84a71.dat . Это UUID, сервер его и использует.
Про смену ника: да, я тут снова сократил скрипт. В идеале, изменив ник ты не теряешь своего UUID. Но скрипт сверху проверяет UUID как md5 от ника игрока. На самом деле UUID у меня формируется из почты, потому что она не меняется. Ник персонажа мы можем менять как хотим, а UUID - остается неизменной и сервер загружает нужное сохранение.
[merge_posts_bbcode]Добавлено: 23.05.2015 16:11:41[/merge_posts_bbcode]

GloomyFolken написал(а):
О, дописал, клёво. А в какой момент сервер чекает авторизацию игрока? Просто ждет некоторое время после "знакомства" или ему клиент отправляет "вуаля, я договорился с сервером авторизации, чекай"?
  • Клиент : Тааак... Сайт ничего не ответил, значит все хорошо. Эй, Сервер! Я хочу подключиться!!!
  • Сервер : Падажжи, ебана, сейчас спрошу кто ты у Сайта. Эй, Сайт, тут некто username хочет ко мне подключиться. Пробей по базе.
  • Сайт : Таак, был у меня такой недавно, приходил, твой serverId показывал. Он не в бане, впускай его.
  • Сервер : Оке, Сайт. Милости прошу к нашему шалашу.
 
Ахах, хорошо описал взаимодействие :D
Если оно так работает, то ИМХО нужно добавить после седьмого пункта, что клиент шлёт ещё один запрос серверу.
 
Ибо сейчас новые версии конвертят старые сейвы из старой папки players в новые, в папку playerdata
 
Dragon2488 написал(а):
Ибо сейчас новые версии конвертят старые сейвы из старой папки players в новые, в папку playerdata
Ясно. Я, к сожалению, почти не работал с версиями старше 1.6.4. Только чутка позаимствовал из 1.7 некоторые интересные решения вроде системы передачи пакетов и замены всякой фигни вроде плеер трекеров с тик хендлерами на события.
 
Нехилый гайд.
 
Dovakin написал(а):
Дракон говорил про --password.Прокатит ли тут?
Пару mojang_account : password обрабатывает другой скрипт. Предположу, что этот
Код:
https://authserver.mojang.com/authenticate

Как обработать подобный запрос можно почитать тут: http://wiki.vg/Authentication#Authenticate
На днях попробую.
 
Dovakin написал(а):
Мне интересно стало,uuid в скриптах должен меняться или нет?
UUID - Universally Unique Identifier (Универсальный Уникальный Идентификатор)
UUID никогда не должен менятся. С помощью UUID происходит сохранение игроков, их инвентаря и привата.
 
А если удастся изменить UUID игрока Вася на UUID игрока Пети?Все имущество Пети перейдет к рукам Васи?
 
XuPuPG написал(а):
А если удастся изменить UUID игрока Вася на UUID игрока Пети?Все имущество Пети перейдет к рукам Васи?
Вася перестанет существовать и станет Петей.
 
Назад
Сверху