Продолжение написания крипто игры на ue5 (Работа с запросами и шифрование AES)

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0

Предисловие:​

Данная статья продолжает пошаговую инструкцию по разработке онлайн крипто-игры в стилистике MMORPG. В этой части будет показано, как реализовать сервер регистрации на Python с шифрованием паролей в базе данных. Приведённый ниже код отлично подойдёт для других проектов, таких как сайты и приложения а также использование игрового движка как инсталлер ваших файлов.

Кроме того, в статье будет объяснено, как работать с JSON: создавать JSON-объекты и читать их. Также будет рассмотрен процесс отправки запросов с клиента на сервер и обратно — это решение также можно использовать в других проектах, не связанных с геймдевом.

Помимо этого, в статье будет описана реализация онлайн-составляющей, включая настройку выделенного сервера (dedicated server), синхронизацию действий разных клиентов на одном сервере.

Прежде чем приступить к работе с мультиплеерной составляющей, необходимо выбрать, как будет организован мультиплеер. Существует два основных подхода:

  1. Первый вариант — это когда один из клиентов одновременно выступает и как клиент, и как сервер, а остальные устройства подключаются к нему как клиенты. Основной недостаток этого метода заключается в том, что если клиент, выполняющий роль сервера, выйдет из игры, то все остальные клиенты также потеряют соединение. Этот подход лучше всего подходит для кооперативных игр с друзьями в лобби.
  2. Второй вариант — это использование выделенного сервера (dedicated server), когда сервер запускается как отдельная программа, а все клиенты подключаются к нему. Этот метод более надежен и лучше подходит для проектов, рассчитанных на многопользовательский режим, таких как наш проект.
Для текущего проекта единственным подходящим способом является использование выделенного сервера (dedicated server).

Основы работы с dedicated server​

Для тестов внутри движка пока что не потребуется скомпилировать отдельный сервер, и его можно сразу запустить через Unreal Engine. Для этого нужно перейти на игровую сцену, выбрать сетевой режим и указать количество запущенных клиентов.
1725194203080.png


Если на данном этапе запустить игру, то откроются два окна с клиентами, и передвижение, а также анимации передвижения будут реплицированы по умолчанию, так как передвижение осуществляется с использованием функции Add Movement, которая уже реплицирована.

Что такое репликация​

Если говорить простыми словами, репликация — это процесс, при котором действие, выполненное на одной системе, повторяется на другой системе. Таким образом достигается синхронизация между сервером и клиентами. Клиент выполняет действие и получает его результаты, сервер повторяет это действие и получает те же результаты, а затем сервер вызывает мультикаст для распространения действия на остальных клиентов, чтобы у них были те же результаты.

Что такое мультикаст​

Это один из видов репликации, позволяющий вызвать функцию на всех подключенных клиентах. Данный тип репликации может быть вызван только сервером.

В данный момент практически всё управление персонажем реплицировано по умолчанию, остаётся лишь реализовать репликацию поворота на месте и наклона спины за камерой вверх и вниз. Для начала можно рассмотреть простой график, который объяснит, как будет подключаться репликация:
1725194233396.png


Синхронизация действий клиентов​

Теперь можно приступить к практическому применению полученных знаний. Для начала нужно перейти в BluePrint персонажа, в котором выстроена основная логика перемещения. Далее нужно найти блок, где реализована логика поворота мышью. Рядом с ним нужно вызвать кастомные события (Custom Event); их потребуется два, и назвать их можно как SERVER и MULTICAST.
1725194255850.png


В событии сервера нужно установить свойство копирования события с клиента на сервер. Так как событие поворота также передаёт переменную типа float, в кастомный ивент нужно добавить слот для хранения этой переменной.
1725194276986.png


1725194299485.png



В событии мультикаста нужно установить свойство вещания, то есть мультикаст. Также нужно добавить слот под float переменную, значения которой поступают из ивента InputAxis LookRL. Эти значения будут передаваться от клиента к серверу, а затем от сервера к мультикасту.
1725194325130.png


Теперь нужно подключить ивент поворота к SERVER, затем SERVER к MULTICAST, а на мультикасте выполнить основную логику.
1725194342938.png



По аналогии нужно реализовать репликацию и для поворота на месте.
1725194364447.png


Сервер регистрации на Python​

Теперь можно начать этап регистрации игроков и авторизации в игре. Первым делом будет реализована регистрация игрока; сервер регистрации будет написан на Python Flask. На данный момент сервер регистрации будет представлять собой простую страницу в браузере для ввода логина и пароля, а также для записи этих данных в базу данных SQLite.

Подготовка проекта​

Для начала нужно подготовить проект сервера регистрации. Для этого потребуется основная папка, в которой будет находиться файл main.py. В этой папке также должна быть папка templates, а в папке templates — два HTML-файла: registration.html и profile.html.

Так как проект будет использовать веб-фреймворк Flask, нужно установить Flask командой pip install flask. Кроме того, сервер будет работать с базой данных SQLite, и для работы с ней можно установить дополнительный модуль Flask под названием flask-sqlalchemy.
Python: Скопировать в буфер обмена
Код:
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy

Теперь нужно инициализировать Flask и SQLAlchemy, указать путь к базе данных, а также задать ключ для шифрования куки-сессий Flask, в которых хранятся данные, такие как информация об авторизации.
Python: Скопировать в буфер обмена
Код:
app = Flask(__name__)
app.secret_key = 'supersecretkey'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

Теперь нужно определить структуру таблиц в базе данных (в скобках указаны свойства, такие как ограничения количества символов и тип записываемых данных).
Python: Скопировать в буфер обмена
Код:
class User(db.Model):
   id = db.Column(db.Integer, primary_key=True)
   username = db.Column(db.String(80), unique=True, nullable=False)
   password = db.Column(db.String(500), nullable=False)
P.S. Максимальное количество символов в пароле такое большое для того, чтобы в дальнейшем хранить зашифрованные пароли, так как зашифрованные пароли гораздо длиннее обычных.

Также нужно определить маршруты для страницы авторизации и регистрации, а также для страницы профиля.
Python: Скопировать в буфер обмена
Код:
@app.route('/', methods=['GET'])
def registration():
   return render_template('registration.html')


@app.route('/profile')
def profile():
   return render_template('profile.html')

И так как данные будут шифроваться, а именно на текущий момент будет шифроваться только пароль, понадобится функция для шифрования и расшифровки.

Работа с шифрованием и расшифровкой​

Для шифрования будет использоваться метод AES-128.

Как работает шифрование AES​

Данный метод очень популярен и используется во многих программах; например, в браузерах данные шифруются именно таким образом. Этапов шифрования данным методом достаточно немного. Сначала берутся исходные данные, затем выбирается режим шифрования данных (в данном случае будет использован режим CBC), после этого выбирается ключ для шифрования. Далее берутся данные для шифрования, и на выходе получаются два объекта: зашифрованные данные и вектор инициализации (IV).

Что такое CBC​

Это один из режимов шифрования, который позволяет шифровать одинаковые блоки данных, получая при этом разные вариации зашифрованных данных. Это улучшает защиту, так как невозможно расшифровать данные методом перебора. При использовании данного метода результатом работы будут не только зашифрованные данные, но и вектор инициализации (IV).

Что такое IV​

IV — это набор случайных данных, которые добавляются к чистым незашифрованным данным, чтобы каждый раз результат шифрования был разным.

Написание логики сервера регистрации​

Для начала нужно установить и импортировать необходимые библиотеки. Для этого будет использоваться библиотека pycryptodome.

P.S. Существует также библиотека pycryptodomex, которая по сути является той же библиотекой, но по неизвестным причинам она не подходит. Поэтому важно не перепутать и установить правильную библиотеку. Если будут установлены обе, они могут конфликтовать, и ничего работать не будет, что потребует удаления лишней библиотеки.
Python: Скопировать в буфер обмена
Код:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64

После импортирования библиотек нужно создать переменную, в которой будет храниться ключ для шифрования пароля (в дальнейшем шифроваться также будут криптокошельки, но для них будет использоваться отдельный ключ для большей безопасности).

P.S. Ключ для шифрования должен иметь размер 16, 24 или 32 байта. Для AES-128 используется 16-байтовый ключ.
Python: Скопировать в буфер обмена
KEY_PASSWORD = b'1234567887654321'

Функция шифрования​

Теперь нужно написать функцию для шифрования данных, которая будет принимать один аргумент, а именно данные для шифрования.
Python: Скопировать в буфер обмена
Код:
def encrypt(data):
cipher = AES.new(KEY_PASSWORD, AES.MODE_CBC)
ct_bytes = cipher.encrypt(pad(data.encode(), AES.block_size))
iv = base64.b64encode(cipher.iv).decode('utf-8')
ct = base64.b64encode(ct_bytes).decode('utf-8')
return iv, ct
1 строка: KEY_PASSWORD — это ключ для шифрования и расшифровки, AES.MODE_CBC — это режим шифрования.

2 строка: cipher.encrypt — это вызов объекта с параметрами для шифрования и само шифрование. pad — это добавление байтов к нешифрованному паролю, чтобы он состоял из блоков данных, каждый из которых равен 16 байтам; это можно назвать выравниванием пароля. data.encode() — это преобразование полученных данных из переменной в байты. AES.block_size обеспечивает правильное дополнение нешифрованного пароля, чтобы все блоки были по 16 байт.

3 строка: base64.b64encode — это конвертация в формат base64. cipher.iv — это вектор инициализации, который будет конвертирован. decode('utf-8') конвертирует байтовую строку в обычную.

4 строка: Аналогично третьей строке, в этой строке происходит конвертация уже самих зашифрованных данных.

Функция расшифровки​

Теперь потребуется функция для расшифровки, которая будет принимать два аргумента: IV и зашифрованные данные.
Python: Скопировать в буфер обмена
Код:
def decrypt(iv, encrypted_data):
   iv = base64.b64decode(iv)
   ct = base64.b64decode(encrypted_data)
   cipher = AES.new(KEY_PASSWORD, AES.MODE_CBC, iv)
   pt = unpad(cipher.decrypt(ct), AES.block_size)
   return pt.decode()
1 и 2 строка: IV и зашифрованные данные декодируются из base64 обратно в их исходный вид.

3 строка: Создаётся объект шифрования с ключом расшифровки (KEY_PASSWORD), режимом шифрования (AES.MODE_CBC) и IV.

4 строка: Расшифровка ct (зашифрованные данные). unpad(..., AES.block_size) означает удаление лишних байтов, которые были добавлены для выравнивания блоков при шифровании данных, чтобы каждый блок был ровно по 16 байтов.

5 строка: Возвращает расшифрованные данные и переводит их из байтов в строку.

Написание веб части​

Теперь можно приступить к созданию страницы регистрации и авторизации. HTML-файл был подготовлен ранее и называется registration.html. В нём нужно будет создать форму с полями ввода и кнопкой для отправки данных. Так как при создании HTML-файла через PyCharm заготовка страницы создаётся автоматически, в файле сразу будет этот код:
HTML: Скопировать в буфер обмена
Код:
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Registration</title>
</head>
<body>


</body>
</html>

Первым делом будет реализована форма для регистрации, использующая метод POST. Это означает, что данные из формы будут отправляться POST-запросом в функцию, которая будет обрабатывать полученные данные. В данном случае такой функцией является register_user.

P.S. При нажатии на кнопку с типом submit данные из формы отправляются на адрес register_user.
HTML: Скопировать в буфер обмена
Код:
<form method="POST" action="{{ url_for('register_user') }}">
   <label>Логин:</label>
   <input type="text" name="username" required>
   <br>
   <label>Пароль:</label>
   <input type="password" name="password" required>
   <br>
   <input type="submit" value="Зарегистрироваться">
</form>

Точно так же нужно реализовать отправку данных для авторизации. Для этого нужно создать форму с полями ввода, но отправка будет происходить в функцию login_user.
HTML: Скопировать в буфер обмена
Код:
<form method="POST" action="{{ url_for('login_user') }}">
   <label>Логин:</label>
   <input type="text" name="username" required>
   <br>
   <label>Пароль:</label>
   <input type="password" name="password" required>
   <br>
   <input type="submit" value="Войти">
</form>

Также на страницу будут выводиться сообщения с Python-части сервера об удачных и неудачных действиях, таких как успешная расшифровка пароля, удачная регистрация и т.д.
Python: Скопировать в буфер обмена
Код:
{% with messages = get_flashed_messages() %}
   {% if messages %}
       <ul>
           {% for message in messages %}
               <li>{{ message }}</li>
           {% endfor %}
       </ul>
   {% endif %}
{% endwith %}
В данном коде вызывается функция get_flashed_messages() для получения flash-сообщений, хранящихся в хранилище сообщений, и результат вызова записывается в переменную messages. После этого идёт условие if, которое срабатывает, если в переменной messages есть сообщения, и выводит их на странице. Внутри if вызывается цикл for, в котором перебираются все сообщения из переменной messages и вставляются в переменную message. Затем выводится значение из переменной message прямо на страницу.

Также нужно подготовить страницу профиля. В данный момент она будет представлять собой пустую страницу с выводом приветственного текста. Файл данной страницы был также ранее заготовлен и называется profile.html.
HTML: Скопировать в буфер обмена
Код:
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Profile</title>
</head>
<body>
   <h2>Добро пожаловать на страницу профиля!</h2>
</body>
</html>

Python функции​

Теперь нужно реализовать две функции. Одна будет предназначена для обработки данных из формы регистрации, а вторая — для обработки данных из формы авторизации.

Регистрация​

Первой на очереди будет функция регистрации. Первое, что нужно в ней сделать, это принять данные из формы и назначить полученные данные переменным.
Python: Скопировать в буфер обмена
Код:
@app.route('/register_user', methods=['POST'])
def register_user():
   username = request.form['username']
   password = request.form['password']

Далее нужно проверить полученный логин на совпадение в базе данных. Если такой логин уже присутствует, необходимо вывести уведомление об этом на веб-странице (ранее в файле registration.html был написан код для отображения подобных уведомлений). После вывода уведомления происходит редирект на страницу регистрации.
Python: Скопировать в буфер обмена
Код:
existing_user = User.query.filter_by(username=username).first()
if existing_user:
   flash('Аккаунт с таким логином уже существует. Попробуйте другой логин.')
   return redirect(url_for('registration'))
P.S. flash — это функция во Flask, которая сохраняет сообщение в специальном хранилище для сообщений внутри сессии. После отображения сообщения оно удаляется из хранилища.

Далее, если логин в базе данных не обнаружен, следует использовать этот код:
Python: Скопировать в буфер обмена
Код:
iv, encrypted_password = encrypt(password)
new_user = User(username=username, password=f'{iv}:{encrypted_password}')
db.session.add(new_user)
db.session.commit()
return redirect(url_for('profile'))
1 строка: Функция encrypt принимает пароль, полученный с веб-страницы, и возвращает значения iv и зашифрованный пароль. Эти значения сохраняются в переменные iv и encrypted_password.

2 и 3 строка: Добавление в базу данных полученного с веб-страницы логина и запись в столбец password значения iv и зашифрованного пароля, разделённых двоеточием, полученных из функции шифрования.

4 строка: Сохранение всех изменений в базе данных.

5 строка: После сохранения данных в базе данных происходит редирект на страницу profile.

Авторизация​

Теперь будет показана реализация функции авторизации. Первое, что нужно сделать, это также добавить переменные, в которые будут записываться логин и пароль из формы веб-страницы, а также провести проверку на наличие введённого логина в базе данных.
Python: Скопировать в буфер обмена
Код:
@app.route('/login_user', methods=['POST'])
def login_user():
   username = request.form['username']
   password = request.form['password']


   user = User.query.filter_by(username=username).first()

Далее идет условие if: если логин найден, происходит расшифровка пароля из базы данных.

P.S. Возможно, у кого-то возникнет вопрос, почему бы не зашифровать полученный пароль и сравнить их в зашифрованном виде. Это невозможно, потому что при написании функции шифрования был использован метод, который выдаёт каждый раз разный результат при шифровании для большей защиты данных.

Python: Скопировать в буфер обмена
Код:
if user:
   iv, encrypted_password = user.password.split(':', 1)
   try:
       decrypted_password = decrypt(iv, encrypted_password)
   except (ValueError, KeyError) as e:
       flash('Ошибка при проверке пароля. Попробуйте снова.')
       return redirect(url_for('registration'))
2 строка: Разделение зашифрованного пароля на IV и зашифрованный пароль. ':' означает, что строка будет разделена по этому символу, 1 означает, что строка будет разделена один раз. Это гарантирует, что из строки получится два объекта: iv и пароль.

4 строка: Вызывается функция decrypt, в которую передаются два значения: iv и зашифрованный пароль. Функция расшифровки возвращает расшифрованный пароль, который записывается в переменную decrypted_password.

6 строка: Вывод сообщения на веб-страницу с использованием flash, если пароль не удалось расшифровать.

7 строка: Если пароль не подошел, происходит редирект на страницу регистрации.

Далее идет код с условием: если расшифрованный пароль из базы данных совпадает с паролем, полученным с веб-страницы, то происходит редирект на страницу профиля. На этом функция авторизации завершена.
Python: Скопировать в буфер обмена
Код:
 if decrypted_password == password:
       return redirect(url_for('profile'))
   else:
       flash('Неверный пароль. Попробуйте снова.')
else:
   flash('Пользователь с таким логином не найден.')

Теперь можно указать, что будет запускаться при запуске программы, а именно: создание базы данных в контексте приложения во Flask (app — это инициализированный объект Flask, app.app_context() — это контекст приложения). Также в самом конце запускается сам Flask с флагом debug, благодаря которому как минимум выводятся подробные ошибки.
Python: Скопировать в буфер обмена
Код:
if __name__ == '__main__':
   with app.app_context():
       db.create_all()
   app.run(debug=True)

Ограничение входа на страницу профиля без аккаунта​

Теперь регистрация и авторизация готовы. Однако от страницы профиля мало толку, так как перейти на неё можно без прохождения регистрации или авторизации. Чтобы это исправить, нужно найти функцию регистрации и в ней найти строку:
Python: Скопировать в буфер обмена
db.session.commit()

После этой строки нужно добавить маркер того, что авторизация прошла успешно (маркер сохраняется в сессии Flask. В куки браузера записывается идентификатор сессии, который связывается с сервером. Сервер использует этот идентификатор для получения данных из сессии, таких как этот маркер авторизации).
Python: Скопировать в буфер обмена
session['logged_in'] = True

Далее нужно найти функцию авторизации и в ней проверить совпадение пароля из базы данных с паролем из формы веб-страницы.
Python: Скопировать в буфер обмена
if decrypted_password == password:

После этой строки нужно также установить маркер авторизации.
Python: Скопировать в буфер обмена
session['logged_in'] = True

Теперь нужно найти функцию, которая определяет маршрут на страницу профиля, и добавить в неё условие проверки: если маркер отсутствует или равен False, перенаправлять на страницу авторизации.
Python: Скопировать в буфер обмена
Код:
@app.route('/profile')
def profile():
   if not session.get('logged_in'):
       return redirect(url_for('registration'))
   return render_template('profile.html')
P.S. В условии не нужно указывать что-то типа == False, так как если ключ отсутствует или его значение равно False, значение маркера в любом случае будет отрицательным. Поэтому not подходит под оба варианта. После этих изменений на страницу профиля можно попасть только если пользователь успешно прошел регистрацию или авторизацию.

На данный момент всё закончено, и можно перейти к проекту игры на UE.

Создание меню авторизации в игровом движке​

1725195135983.png


Показывать как создать верстку интерфейса я не буду и сразу покажу как настроить отправку запросов на Python сервер.

Логика взаимодействия игры с Python сервером​

Теперь нужно начать разрабатывать логику подключения к серверу регистрации. Для этого нужно Скачать

View hidden content is available for registered users!
 
Сверху Снизу