[Гайд][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;
  • Вы научились использовать протокол в своих целях;
  • Теперь у вас есть потенциал к дальнейшему обучению и самостоятельному исследованию. Поздравляю.

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

 
808
3
124
Хорошо написано, я за перенос в учебник.
Пара вопросов:
1) У тебя написано: 
Клиент запрашивает у Сервера параметр ServerID - уникальный номер для подключения;
а потом
Сервер также запрашивает разрешение у Сайта авторизации, отдавая ему Ник игрока и свой ServerID;
Откуда при таком раскладе у сервера информация об игроке, например, ник? И в какой момент сервер делает этот запрос? Я не шарю в теме, но имхо пропущен какой-то шаг. Например, после запроса от клиента к сайту клиент ещё раз коннектится к серверу уже в попытке подключиться и при этом отдает ему ник игрока.
2) Как смена ника работает с сейвами? ЕМНИП даже в 1.7 в качестве названия для сейва используется ник игрока, а не UUID. Или я путаю?
 
329
13
GloomyFolken написал(а):
Хорошо написано, я за перенос в учебник.
Пара вопросов:
1) У тебя написано: 
Клиент запрашивает у Сервера параметр ServerID - уникальный номер для подключения;
а потом
Сервер также запрашивает разрешение у Сайта авторизации, отдавая ему Ник игрока и свой ServerID;
Откуда при таком раскладе у сервера информация об игроке, например, ник? И в какой момент сервер делает этот запрос? Я не шарю в теме, но имхо пропущен какой-то шаг. Например, после запроса от клиента к сайту клиент ещё раз коннектится к серверу уже в попытке подключиться и при этом отдает ему ник игрока.
2) Как смена ника работает с сейвами? ЕМНИП даже в 1.7 в качестве названия для сейва используется ник игрока, а не UUID. Или я путаю?
  1. Не торопись, юный падаван, Сервер узнает информацию о Клиенте чуть позже. Терпение...
  2. Попробуй побегать на сервере версий 1.7.2+ и глянь просмотрщиком NBT получившееся сохранение. ЕМНИП, там уже лежат UUID вместо ников.
 
808
3
124
1) На восьмом шаге у сервера откуда-то уже есть ник игрока. При этом единственное взаимодействие с сервером до этого - это запрос от клиента на serverID. Откуда у сервера берется ник?
2) В НБТ UUID лежит как минимум с 1.6.4. Проблема в том, что название сейва - это ник игрока. Не будет же сервер перебирать все сейвы в поисках того, где лежит нужный UUID. Как работает смена ника? Игрок вайпается?
 
1,990
18
105
kiss_3kb.1432378729.png
 
808
3
124
О, дописал, клёво. А в какой момент сервер чекает авторизацию игрока? Просто ждет некоторое время после "знакомства" или ему клиент отправляет "вуаля, я договорился с сервером авторизации, чекай"?
 
329
13
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 показывал. Он не в бане, впускай его.
  • Сервер : Оке, Сайт. Милости прошу к нашему шалашу.
 
808
3
124
Ахах, хорошо описал взаимодействие :D
Если оно так работает, то ИМХО нужно добавить после седьмого пункта, что клиент шлёт ещё один запрос серверу.
 
2,955
12
Ибо сейчас новые версии конвертят старые сейвы из старой папки players в новые, в папку playerdata
 
808
3
124
Dragon2488 написал(а):
Ибо сейчас новые версии конвертят старые сейвы из старой папки players в новые, в папку playerdata
Ясно. Я, к сожалению, почти не работал с версиями старше 1.6.4. Только чутка позаимствовал из 1.7 некоторые интересные решения вроде системы передачи пакетов и замены всякой фигни вроде плеер трекеров с тик хендлерами на события.
 
471
5
Нехилый гайд.
 
329
13
Dovakin написал(а):
Дракон говорил про --password.Прокатит ли тут?
Пару mojang_account : password обрабатывает другой скрипт. Предположу, что этот
Код:
https://authserver.mojang.com/authenticate

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