Регистрация и авторизация CRMP — MySQL, пароли и защита
Система регистрации и авторизации — одна из важнейших частей игрового мода CRMP. Она определяет, как создаются аккаунты, проверяются пароли, загружаются персонажи и сохраняется прогресс игроков.
Надёжная система должна не только показывать диалог входа. Необходимо правильно организовать таблицы MySQL, асинхронные запросы, хеширование паролей, защиту от перебора, обработку повторных подключений и сохранение данных при выходе или перезапуске сервера.
В руководстве рассмотрена настройка регистрации и авторизации на сервере CRMP, работа с MySQL, структура таблицы аккаунтов, безопасное хранение паролей, загрузка и сохранение данных, защита SQL-запросов и устранение распространённых ошибок.
Подготовка базы данных перед созданием аккаунтов:
Установка необходимых серверных расширений:
Установка основного игрового режима:
Как устроена регистрация и авторизация CRMP
После подключения игрока сервер получает его имя и проверяет наличие соответствующего аккаунта в базе данных.
Если аккаунт не найден
- Игроку показывается диалог регистрации.
- Пароль проходит проверку длины и формата.
- Пароль передаётся алгоритму хеширования.
- В базу записывается хеш, а не исходный пароль.
- Создаётся запись аккаунта.
- Игрок переходит к созданию персонажа или входит в игру.
Если аккаунт существует
- Игроку показывается диалог авторизации.
- Введённый пароль проверяется по сохранённому хешу.
- При совпадении загружаются данные аккаунта.
- Сервер помечает игрока как авторизованного.
- Игроку разрешается использовать игровые системы.
До окончания авторизации необходимо запретить
- появление персонажа в игровом мире;
- использование команд;
- управление транспортом;
- получение денег и предметов;
- доступ к административным функциям;
- запуск сохранения незагруженных данных.
Что потребуется для системы аккаунтов
Серверная часть
- работающее серверное ядро CRMP;
- игровой мод в формате PWN и AMX;
- MySQL или MariaDB;
- совместимый MySQL-плагин;
- include-файл MySQL;
- плагин безопасного хеширования паролей;
- отладочный плагин для поиска ошибок;
- доступ к серверным журналам.
Для компиляции
pawno/include/a_mysql.inc
pawno/include/bcrypt.inc
pawno/include/другие_библиотеки.inc
Для запуска Windows
plugins/mysql.dll
plugins/bcrypt.dll
plugins/crashdetect.dll
Для запуска Linux
plugins/mysql.so
plugins/bcrypt.so
plugins/crashdetect.so
Структура базы данных авторизации
Не следует хранить все данные сервера в одной огромной таблице. Для небольшого мода это может работать, но дальнейшее развитие и обслуживание заметно усложнятся.
Практическое разделение
accounts— вход, пароль и служебные данные;characters— игровые персонажи;vehicles— личный транспорт;houses— дома;businesses— бизнесы;inventory— предметы;punishments— наказания;admin_logs— действия администрации;login_attempts— подозрительные попытки входа.
Преимущества разделения
- понятная структура;
- меньше дублирования;
- удобное обновление схемы;
- проще создавать резервные копии;
- легче находить ошибки;
- можно поддерживать несколько персонажей.
Создание таблицы аккаунтов
Ниже приведён базовый пример. Его необходимо адаптировать под используемый игровой мод и версию MySQL.
CREATE TABLE `accounts` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(24) NOT NULL,
`password_hash` VARCHAR(255) NOT NULL,
`email` VARCHAR(254) DEFAULT NULL,
`registration_ip` VARCHAR(45) DEFAULT NULL,
`last_ip` VARCHAR(45) DEFAULT NULL,
`failed_attempts` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
`locked_until` DATETIME DEFAULT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_login_at` DATETIME DEFAULT NULL,
`updated_at` DATETIME NOT NULL
DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_accounts_name` (`name`),
UNIQUE KEY `uq_accounts_email` (`email`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
Обязательные поля
id— внутренний идентификатор;name— игровое имя;password_hash— результат безопасного хеширования;created_at— дата регистрации;last_login_at— дата последнего успешного входа.
Дополнительные поля
- электронная почта;
- язык игрока;
- настройки интерфейса;
- число ошибочных попыток;
- временная блокировка входа;
- дата изменения пароля;
- статус подтверждения контакта.
Разделение аккаунта и персонажа
Аккаунт отвечает за доступ к серверу, а персонаж содержит игровой прогресс.
Пример таблицы персонажей
CREATE TABLE `characters` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`account_id` BIGINT UNSIGNED NOT NULL,
`name` VARCHAR(24) NOT NULL,
`level` INT UNSIGNED NOT NULL DEFAULT 1,
`experience` INT UNSIGNED NOT NULL DEFAULT 0,
`money` BIGINT NOT NULL DEFAULT 0,
`bank_money` BIGINT NOT NULL DEFAULT 0,
`health` FLOAT NOT NULL DEFAULT 100.0,
`armour` FLOAT NOT NULL DEFAULT 0.0,
`position_x` FLOAT NOT NULL DEFAULT 0.0,
`position_y` FLOAT NOT NULL DEFAULT 0.0,
`position_z` FLOAT NOT NULL DEFAULT 3.0,
`position_angle` FLOAT NOT NULL DEFAULT 0.0,
`skin` INT NOT NULL DEFAULT 0,
`interior` INT NOT NULL DEFAULT 0,
`virtual_world` INT NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL
DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_characters_name` (`name`),
KEY `idx_characters_account_id` (`account_id`),
CONSTRAINT `fk_characters_account`
FOREIGN KEY (`account_id`)
REFERENCES `accounts` (`id`)
ON DELETE CASCADE
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
Такое разделение позволяет
- создать несколько персонажей на одном аккаунте;
- менять имя персонажа без изменения данных входа;
- отдельно хранить настройки пользователя;
- проще восстанавливать игровой прогресс;
- снизить риск случайного изменения пароля игровым запросом.
Для простого сервера с одним персонажем можно хранить игровые поля в accounts, но структуру желательно продумать заранее.
Индексы и уникальные значения
Имя аккаунта должно быть уникальным
UNIQUE KEY `uq_accounts_name` (`name`)
Уникальный индекс защищает от создания двух одинаковых аккаунтов даже при одновременных запросах.
Не полагайтесь только на SELECT перед INSERT
Два подключения могут одновременно проверить отсутствие записи и попытаться выполнить регистрацию. Окончательную защиту должен обеспечивать уникальный индекс.
Индекс идентификатора аккаунта
KEY `idx_characters_account_id` (`account_id`)
Он ускоряет загрузку персонажей конкретного аккаунта.
Выбор регистра имени
Заранее определите, должны ли следующие имена считаться одинаковыми:
Player_Name
player_name
PLAYER_NAME
Поведение зависит от сравнения столбца. Для большинства серверов безопаснее не разрешать регистрацию отличающихся только регистром аккаунтов.
Совместимость MySQL и bcrypt-плагинов
Игровой мод, include-файл и нативный плагин должны использовать совместимое API.
Проверяйте комплект
plugins/mysql.dll или mysql.so
pawno/include/a_mysql.inc
gamemodes/roleplay.amx
Для хеширования
plugins/bcrypt.dll или bcrypt.so
pawno/include/bcrypt.inc
gamemodes/roleplay.amx
Признаки несовместимости
Run time error 19;- отсутствующие native-функции;
- плагин получает статус Failed;
- callback хеширования не вызывается;
- проверка любого пароля завершается ошибкой;
- сервер закрывается при регистрации;
- AMX работает только со старой библиотекой.
После замены include необходимо повторно скомпилировать PWN и убедиться, что на сервер загружен новый AMX.
Состояние авторизации игрока
Для каждого подключённого игрока сервер должен хранить текущее состояние процесса входа.
Пример перечисления Pawn
enum E_AUTH_STATE
{
AUTH_STATE_NONE,
AUTH_STATE_CHECKING,
AUTH_STATE_REGISTER,
AUTH_STATE_HASHING,
AUTH_STATE_LOGIN,
AUTH_STATE_VERIFYING,
AUTH_STATE_LOADING,
AUTH_STATE_AUTHORIZED
};
new E_AUTH_STATE:g_PlayerAuthState[MAX_PLAYERS];
new bool:g_PlayerLoggedIn[MAX_PLAYERS];
new g_PlayerAccountId[MAX_PLAYERS];
При подключении сбрасывайте данные
g_PlayerAuthState[playerid] = AUTH_STATE_NONE;
g_PlayerLoggedIn[playerid] = false;
g_PlayerAccountId[playerid] = 0;
До успешного входа проверяйте
if (!g_PlayerLoggedIn[playerid])
{
SendClientMessage(
playerid,
-1,
"Сначала завершите авторизацию."
);
return 0;
}
Проверка должна присутствовать не только в командах, но и в обработчиках транспорта, диалогов, пикапов и игровых действий.
Проверка аккаунта при подключении
После входа игрока сервер получает его имя и выполняет асинхронный запрос.
Общий алгоритм
- Сбросить старое состояние слота.
- Получить имя игрока.
- Проверить допустимый формат имени.
- Создать безопасный SQL-запрос.
- Отправить запрос асинхронно.
- Дождаться callback.
- Показать регистрацию или авторизацию.
Пример структуры запроса
SELECT
`id`,
`password_hash`,
`failed_attempts`,
`locked_until`
FROM `accounts`
WHERE `name` = ?
LIMIT 1;
Символ ? показывает место параметра в подготовленном запросе. Конкретный синтаксис зависит от используемого MySQL-плагина.
Если плагин не поддерживает параметры
Используйте функцию форматирования с обязательным экранированием строк. Асинхронное выполнение само по себе не защищает от SQL-инъекций.
Настройка диалога регистрации
Пример идентификатора
#define DIALOG_REGISTER 1000
Общий пример
ShowPlayerDialog(
playerid,
DIALOG_REGISTER,
DIALOG_STYLE_PASSWORD,
"Регистрация",
"Придумайте пароль для нового аккаунта.",
"Создать",
"Выход"
);
При ответе необходимо проверить
- игрок всё ещё подключён;
- его состояние равно регистрации;
- нажата кнопка подтверждения;
- пароль не пустой;
- длина находится в допустимом диапазоне;
- аккаунт не был создан другим запросом;
- хеширование ещё не запущено.
При отмене
Игрока следует корректно отключить или вернуть к начальному экрану. Не оставляйте неавторизованного пользователя в игровом мире.
Настройка диалога авторизации
Пример идентификатора
#define DIALOG_LOGIN 1001
Общий пример
ShowPlayerDialog(
playerid,
DIALOG_LOGIN,
DIALOG_STYLE_PASSWORD,
"Авторизация",
"Введите пароль от аккаунта.",
"Войти",
"Выход"
);
Не раскрывайте лишнюю информацию
Сообщения вроде «пароль правильный, но аккаунт заблокирован» могут помогать подбору данных. Ответы системы должны быть понятными игроку, но не раскрывать внутреннюю структуру защиты.
Не помещайте пароль
- в заголовок диалога;
- в SendClientMessage;
- в серверный журнал;
- в отладочный printf;
- в административные логи;
- в SQL-запрос открытым текстом.
Проверка игрового имени
Имя игрока приходит от клиента и должно рассматриваться как внешнее значение.
Проверяйте
- минимальную и максимальную длину;
- допустимые символы;
- наличие разделителя имени и фамилии, если он требуется;
- запрещённые служебные названия;
- повторяющиеся символы;
- соответствие правилам проекта.
Пример допустимого формата
Name_Surname
Не используйте имя как основной идентификатор
Внутренние связи таблиц должны использовать числовой account_id или character_id. Имя может измениться.
Требования к паролю игрока
Практические требования
- разумная минимальная длина;
- поддержка достаточно длинных паролей;
- разрешение букв, цифр и специальных символов;
- запрет пустых значений;
- запрет пароля, совпадающего с игровым именем;
- проверка наиболее очевидных вариантов;
- отсутствие принудительной обрезки без уведомления.
Не заставляйте игрока использовать только цифры
Четырёхзначные и шестизначные пароли легко перебираются. Система должна поддерживать полноценные длинные парольные фразы.
Не изменяйте регистр введённого пароля
Пароли:
MiamiServer2026
miamiserver2026
должны считаться разными значениями.
Ограничивайте размер входных данных
Слишком длинную строку необходимо отклонить до передачи хеширующему плагину и MySQL.
Безопасное хранение паролей
Нельзя хранить
password=MySecretPassword
Нельзя использовать обратимое шифрование
Если пароль можно расшифровать, утечка ключа раскрывает все аккаунты.
Не рекомендуется использовать быстрый SHA-256
Быстрые хеш-функции предназначены для других задач и позволяют выполнять большое количество попыток перебора.
Предпочтительный порядок
- Argon2id, если доступен совместимый и проверенный плагин.
- scrypt, если доступна надёжная реализация.
- bcrypt для совместимых CRMP и Pawn-сборок.
- PBKDF2 с правильно подобранными параметрами.
Формат столбца
`password_hash` VARCHAR(255) NOT NULL
Длины 255 символов обычно достаточно для строковых форматов современных алгоритмов с параметрами и salt.
Что такое salt и pepper
Salt
Уникальное случайное значение для каждого пароля. Современные библиотеки обычно создают salt самостоятельно и включают его в итоговую строку хеша.
Salt можно хранить рядом с хешем
Его назначение — сделать одинаковые пароли разными в базе и затруднить использование заранее подготовленных таблиц.
Pepper
Дополнительный секрет сервера, который не хранится в таблице аккаунтов.
Где хранить pepper
- в защищённой конфигурации сервера;
- в переменной окружения;
- в системе управления секретами;
- в файле с ограниченными правами.
Асинхронное хеширование пароля
Адаптивное хеширование намеренно требует ресурсов. Выполнение тяжёлой операции в основном серверном потоке способно вызвать задержку игрового цикла.
Правильная последовательность регистрации
- Принять пароль из диалога.
- Проверить его длину.
- Установить состояние
AUTH_STATE_HASHING. - Запустить асинхронное хеширование.
- Получить результат в callback.
- Создать аккаунт в MySQL.
- Очистить временный пароль из памяти.
Версионно-независимая схема Pawn
StartPasswordHash(
playerid,
inputtext
);
Callback результата
public OnPasswordHashCompleted(
playerid,
const passwordHash[]
)
{
if (!IsPlayerConnected(playerid))
{
return 1;
}
if (
g_PlayerAuthState[playerid]
!= AUTH_STATE_HASHING
)
{
return 1;
}
CreateAccountQuery(
playerid,
passwordHash
);
return 1;
}
Создание нового аккаунта в MySQL
Структура безопасного запроса
INSERT INTO `accounts`
(
`name`,
`password_hash`,
`registration_ip`,
`last_ip`,
`created_at`
)
VALUES
(
?,
?,
?,
?,
CURRENT_TIMESTAMP
);
После успешного INSERT
- получите идентификатор созданной записи;
- запишите его в данные игрока;
- сбросьте число неудачных попыток;
- создайте начальные настройки;
- создайте персонажа или покажите меню персонажей;
- запишите событие регистрации без пароля.
Не выдавайте стартовые данные дважды
Бонус, деньги и предметы должны выдаваться только после подтверждённого создания записи. Повторный callback не должен создавать второй комплект имущества.
Используйте идентификатор MySQL
g_PlayerAccountId[playerid] =
insertedAccountId;
Не ищите только что созданный аккаунт повторно по имени, если плагин позволяет получить ID вставленной записи.
Защита от повторной регистрации
Уникальный индекс
UNIQUE KEY `uq_accounts_name` (`name`)
Перед созданием повторно проверьте состояние
- игрок не отключился;
- слот не был занят другим подключением;
- регистрация ещё не завершена;
- запрос не был отправлен повторно;
- имя игрока не изменилось.
Обработайте Duplicate entry
Если MySQL вернула ошибку уникального индекса, не создавайте вторую запись и не считайте регистрацию успешной.
Блокируйте повторное нажатие
После запуска хеширования состояние меняется на:
AUTH_STATE_HASHING
Новые ответы того же диалога должны игнорироваться до завершения операции.
Проверка пароля при авторизации
Введённый пароль не нужно хешировать вручную и сравнивать обычной строкой, если современный алгоритм хранит salt и параметры внутри результата.
Правильная последовательность
- Получить сохранённый хеш из базы.
- Принять пароль из диалога.
- Запустить функцию проверки плагина.
- Дождаться callback.
- Разрешить вход только при успешном результате.
- Очистить временную строку пароля.
Версионно-независимая схема
StartPasswordVerification(
playerid,
inputtext,
g_PlayerPasswordHash[playerid]
);
Callback проверки
public OnPasswordVerified(
playerid,
bool:passwordMatches
)
{
if (!IsPlayerConnected(playerid))
{
return 1;
}
if (
g_PlayerAuthState[playerid]
!= AUTH_STATE_VERIFYING
)
{
return 1;
}
if (!passwordMatches)
{
HandleFailedLogin(playerid);
return 1;
}
LoadAccountData(playerid);
return 1;
}
Названия функций необходимо заменить на API установленного хеширующего плагина.
Загрузка данных аккаунта после входа
Не загружайте всё до проверки пароля
До подтверждения пароля достаточно получить:
- идентификатор аккаунта;
- хеш пароля;
- состояние блокировки;
- число неудачных попыток.
После успешной проверки загружайте
- права доступа;
- настройки аккаунта;
- список персонажей;
- игровые данные выбранного персонажа;
- наказания;
- транспорт и имущество;
- инвентарь.
Пример запроса персонажа
SELECT
`id`,
`level`,
`experience`,
`money`,
`bank_money`,
`health`,
`armour`,
`position_x`,
`position_y`,
`position_z`,
`position_angle`,
`skin`,
`interior`,
`virtual_world`
FROM `characters`
WHERE `account_id` = ?
LIMIT 1;
Проверяйте наличие строки
У аккаунта может отсутствовать персонаж, если регистрация завершилась частично. В таком случае покажите создание персонажа, а не загружайте нулевые данные как существующие.
Безопасность асинхронных callbacks
Пока выполняется MySQL-запрос или хеширование, игрок может отключиться. Его playerid способен получить другой пользователь.
Недостаточно проверить только IsPlayerConnected
Новый игрок может быть уже подключён под тем же номером.
Используйте идентификатор сессии
new g_PlayerSessionId[MAX_PLAYERS];
public OnPlayerConnect(playerid)
{
g_PlayerSessionId[playerid]++;
if (g_PlayerSessionId[playerid] <= 0)
{
g_PlayerSessionId[playerid] = 1;
}
return 1;
}
Передайте sessionId в callback
new sessionId =
g_PlayerSessionId[playerid];
CheckAccountAsync(
playerid,
sessionId
);
Проверка callback
if (
!IsPlayerConnected(playerid)
||
sessionId
!= g_PlayerSessionId[playerid]
)
{
return 1;
}
Дополнительно можно проверить
- имя игрока;
- текущее состояние авторизации;
- идентификатор отправленного запроса;
- ожидаемый этап входа.
Фиксация успешной авторизации
Игрок считается вошедшим только после проверки пароля и полной загрузки обязательных данных.
Успешное состояние
g_PlayerLoggedIn[playerid] = true;
g_PlayerAuthState[playerid] =
AUTH_STATE_AUTHORIZED;
После этого можно
- показать выбор персонажа;
- выполнить SpawnPlayer;
- разрешить команды;
- загрузить имущество;
- запустить таймер сохранения;
- записать успешный вход.
Обновление базы
UPDATE `accounts`
SET
`last_ip` = ?,
`last_login_at` = CURRENT_TIMESTAMP,
`failed_attempts` = 0,
`locked_until` = NULL
WHERE `id` = ?
LIMIT 1;
Не помечайте игрока вошедшим раньше
Если g_PlayerLoggedIn станет true до загрузки данных, команды могут использовать нулевые или чужие значения.
Сохранение данных игрока
Сохраняйте только авторизованного игрока с загруженным идентификатором аккаунта или персонажа.
Обязательная проверка
if (
!g_PlayerLoggedIn[playerid]
||
g_PlayerAccountId[playerid] <= 0
)
{
return 0;
}
Пример обновления персонажа
UPDATE `characters`
SET
`level` = ?,
`experience` = ?,
`money` = ?,
`bank_money` = ?,
`health` = ?,
`armour` = ?,
`position_x` = ?,
`position_y` = ?,
`position_z` = ?,
`position_angle` = ?,
`skin` = ?,
`interior` = ?,
`virtual_world` = ?
WHERE `id` = ?
LIMIT 1;
Используйте ID, а не имя
Сохранение по числовому идентификатору безопаснее и не ломается после изменения ника.
Не сохраняйте данные клиента без проверки
Деньги, уровень и административные права должны изменяться серверной логикой, а не значениями, которые клиент может отправить самостоятельно.
Автоматическое сохранение аккаунтов
Периодическое сохранение уменьшает потерю прогресса при аварии, но слишком частые запросы создают лишнюю нагрузку.
Практический подход
- сохранять критические операции сразу;
- остальные данные сохранять периодически;
- не отправлять UPDATE каждую секунду;
- не обновлять неизменившиеся поля без необходимости;
- распределять сохранения игроков по времени;
- использовать асинхронные запросы.
Критические операции
- покупка дома;
- передача крупной суммы;
- изменение административного уровня;
- смена пароля;
- покупка транспорта;
- выдача наказания.
Не запускайте отдельный частый таймер на каждого игрока
Для большого онлайна удобнее общий таймер, который сохраняет ограниченное количество аккаунтов за один цикл.
Сохранение при отключении игрока
В OnPlayerDisconnect проверяйте
- игрок был авторизован;
- данные были полностью загружены;
- идентификатор аккаунта существует;
- не выполняется уже отправленное финальное сохранение;
- значения находятся в допустимом диапазоне.
После формирования запроса очищайте слот
g_PlayerLoggedIn[playerid] = false;
g_PlayerAccountId[playerid] = 0;
g_PlayerAuthState[playerid] = AUTH_STATE_NONE;
Не полагайтесь только на отключение
При аварии процесса callback отключения может не дать возможности надёжно сохранить данные. Поэтому необходимы периодические сохранения и резервные копии.
При штатном выключении
Перед остановкой игрового мода сохраните всех авторизованных игроков и дождитесь отправки важных запросов.
Защита регистрации от SQL-инъекций
Имя игрока, пароль, электронная почта, промокод и текст из диалога являются внешними данными.
Опасный принцип
SELECT *
FROM accounts
WHERE name = 'ВВОД_ИГРОКА'
Если запрос строится простым объединением строк, специальные символы могут изменить его смысл.
Предпочтительный вариант
SELECT *
FROM accounts
WHERE name = ?
LIMIT 1;
При отсутствии подготовленных запросов
- используйте официальную функцию экранирования плагина;
- задавайте правильный размер буфера;
- проверяйте результат форматирования;
- ограничивайте длину входных строк;
- не создавайте названия таблиц из пользовательского ввода;
- не выполняйте несколько SQL-команд из одного поля.
Асинхронность не является защитой
mysql_tquery или другая асинхронная функция предотвращает блокировку игрового потока, но не делает небезопасную строку безопасной автоматически.
Использование асинхронных MySQL-запросов
Запрос авторизации может ожидать сеть, диск или выполнение другой операции базы. Синхронное ожидание способно остановить игровой цикл.
Асинхронно выполняйте
- проверку существования аккаунта;
- создание аккаунта;
- загрузку персонажа;
- загрузку инвентаря;
- сохранение прогресса;
- запись журналов.
Каждый callback должен проверять
- актуальность сессии;
- подключение игрока;
- текущее состояние авторизации;
- наличие результата;
- число полученных строк;
- ошибки преобразования данных.
Не запускайте одинаковый запрос многократно
Кнопки диалога и команды следует блокировать до завершения предыдущей операции.
Защита от перебора паролей
Считайте неудачные попытки
`failed_attempts`
`locked_until`
После неправильного пароля
- Увеличьте счётчик.
- Запишите время попытки.
- Добавьте небольшую задержку.
- После нескольких ошибок временно заблокируйте вход.
- Запишите событие безопасности.
Пример временной блокировки
UPDATE `accounts`
SET
`failed_attempts` =
`failed_attempts` + 1,
`locked_until` =
DATE_ADD(
CURRENT_TIMESTAMP,
INTERVAL 5 MINUTE
)
WHERE `id` = ?
LIMIT 1;
Не используйте постоянную блокировку после нескольких ошибок
Злоумышленник сможет намеренно блокировать чужие аккаунты. Лучше применять постепенно увеличивающуюся задержку и дополнительную проверку владельца.
Ограничивайте попытки
- на один аккаунт;
- на один IP;
- на одно подключение;
- на короткий временной интервал.
Ограничение времени авторизации
Неавторизованный игрок не должен бесконечно занимать слот сервера.
После подключения запустите таймер
По истечении разумного времени проверьте:
if (!g_PlayerLoggedIn[playerid])
{
Kick(playerid);
}
Таймер должен учитывать сессию
Иначе старый таймер может отключить другого игрока, занявшего тот же playerid.
Отменяйте или игнорируйте таймер после входа
Проверка состояния и session ID обязательна даже при отсутствии функции удаления конкретного таймера.
Безопасное восстановление пароля
Восстановление доступа часто становится слабее основной авторизации.
Не рекомендуется использовать
- секретный вопрос о питомце или городе;
- передачу пароля администратору;
- показ старого пароля из базы;
- сброс по одному игровому нику;
- постоянную универсальную команду восстановления;
- общий код для всех игроков.
Более безопасный вариант
- Игрок запрашивает восстановление.
- Создаётся случайный одноразовый токен.
- В базе хранится хеш токена.
- Токен имеет короткий срок действия.
- Он отправляется через подтверждённый канал.
- После использования токен удаляется.
- Все активные сессии аккаунта завершаются.
Для сервера без электронной почты
Используйте ручное восстановление с подтверждением владения, журналом действий и участием ограниченного круга старших администраторов.
Хранение IP-адресов игроков
Столбец для IPv4 и IPv6
`last_ip` VARCHAR(45) DEFAULT NULL
IP можно использовать для
- диагностики взлома;
- ограничения перебора;
- поиска массовых регистраций;
- уведомления о необычном входе;
- расследования серьёзных нарушений.
IP не является надёжным доказательством личности
Адрес может изменяться, использоваться несколькими людьми, принадлежать мобильной сети, VPN или общему маршрутизатору.
Ограничивайте доступ к журналам
IP-адреса не должны отображаться обычным администраторам или публиковаться на сайте без необходимости.
Защита административных аккаунтов
Администратор должен входить как обычный игрок
Пароль проверяется тем же безопасным механизмом. После входа сервер загружает административный уровень из базы.
Не выдавайте права по нику в PWN
Опасный принцип:
if (!strcmp(name, "Owner_Name"))
{
AdminLevel[playerid] = 10;
}
Права должны храниться в базе
`admin_level` SMALLINT UNSIGNED
NOT NULL DEFAULT 0
Изменение уровня должно записываться
- кто выдал права;
- кому они выданы;
- старый уровень;
- новый уровень;
- дата и время;
- причина.
RCON не заменяет админ-систему
Обычным администраторам не следует сообщать RCON-пароль. Он предоставляет управление серверным процессом и должен храниться отдельно.
Ошибки MySQL в системе авторизации
Access denied for user
Access denied for user
'crmp_user'@'localhost'
Проверьте имя пользователя, пароль, разрешённый Host и права на базу.
Unknown database
Unknown database 'crmp_server'
База не создана или в конфигурации указано неправильное имя.
Table doesn't exist
Table 'crmp_server.accounts'
doesn't exist
SQL-файл не импортирован, импорт завершился ошибкой или мод подключён к другой базе.
Unknown column
Unknown column 'password_hash'
in 'field list'
Структура таблицы не соответствует запросам игрового мода.
Duplicate entry
Duplicate entry 'Player_Name'
for key 'uq_accounts_name'
Аккаунт уже существует либо запрос регистрации был выполнен повторно.
Data too long for column
Размер столбца недостаточен для хеша или другого значения. Для строкового формата пароля обычно используется:
VARCHAR(255)
MySQL server has gone away
Проверьте состояние базы, сеть, время ожидания, размер запросов и работу повторного подключения.
Распространённые ошибки регистрации и входа
Диалог регистрации показывается существующему игроку
- запрос выполняется не к той базе;
- имя экранируется неправильно;
- callback неверно проверяет число строк;
- регистр имени обрабатывается иначе;
- таблица аккаунтов пуста.
Диалог входа показывается новому игроку
- запрос возвращает лишние строки;
- не очищен старый cache;
- не проверен session ID;
- callback от предыдущего игрока применён к новому;
- используется неправильное условие результата.
Любой пароль считается правильным
- игнорируется результат проверки;
- callback всегда возвращает успех;
- сравнивается не та переменная;
- используется незагруженный хеш;
- плагин несовместим;
- игрок помечается вошедшим до проверки.
Правильный пароль не принимается
- хеш обрезан столбцом;
- используется другая версия алгоритма;
- в пароль добавляется лишний пробел;
- строка меняет регистр;
- старый хеш создан другим способом;
- неправильно переданы параметры проверки.
Аккаунт создаётся дважды
- нет уникального индекса;
- диалог обрабатывается несколько раз;
- запрос повторно отправляется из callback;
- нет состояния
AUTH_STATE_HASHING; - создание выполняется до завершения предыдущей операции.
Данные одного игрока загружаются другому
Наиболее вероятная причина — повторное использование playerid и отсутствие проверки session ID в асинхронном callback.
После перезапуска пропадает прогресс
- сохранение не вызывается;
- UPDATE выполняется с неправильным ID;
- запрос завершается ошибкой;
- сервер подключён к другой базе;
- данные сохраняются только при выходе;
- процесс завершается до выполнения запросов.
Пароль виден в server_log.txt
Удалите отладочные сообщения и не записывайте полный запрос регистрации. После такой утечки предложите затронутым игрокам сменить пароли.
Проверка системы регистрации и авторизации
Регистрация
- новому игроку показывается регистрация;
- пустой пароль отклоняется;
- слишком короткий пароль отклоняется;
- длинный допустимый пароль принимается;
- в базе появляется одна запись;
- открытый пароль отсутствует;
- повторная регистрация невозможна.
Авторизация
- существующему игроку показывается вход;
- правильный пароль принимается;
- неправильный пароль отклоняется;
- счётчик ошибок увеличивается;
- после нескольких ошибок действует задержка;
- после успешного входа счётчик сбрасывается;
- до входа игровые команды недоступны.
Асинхронные запросы
- Подключитесь к серверу.
- Запустите регистрацию или вход.
- Отключитесь до получения результата.
- Сразу зайдите другим игроком.
- Убедитесь, что старый callback не применился к новой сессии.
Сохранение
- данные сохраняются периодически;
- данные сохраняются при нормальном выходе;
- покупки сохраняются сразу;
- после перезапуска прогресс загружается;
- неавторизованный слот не записывает нулевые данные;
- аккаунт сохраняется по ID.
Нагрузка
- несколько игроков могут войти одновременно;
- хеширование не вызывает заметных зависаний;
- запросы выполняются асинхронно;
- таймер сохранения не создаёт всплеск запросов;
- в базе отсутствуют медленные повторяющиеся SELECT.
Безопасность
- кавычки в имени не ломают запрос;
- специальные символы не меняют SQL;
- пароль не попадает в журнал;
- RCON не связан с игровым паролем;
- административный уровень нельзя изменить с клиента;
- восстановление доступа записывается в журнал.
Контрольный список защиты аккаунтов
Пароли
- не хранятся открытым текстом;
- не используются в обратимом шифровании;
- обрабатываются адаптивным алгоритмом;
- хеш сохраняется полностью;
- пароль не записывается в журнал;
- администратор не может увидеть исходное значение;
- используется совместимый хеширующий плагин.
MySQL
- игровой мод не работает под root;
- пользователь имеет доступ только к своей базе;
- порт не открыт всему интернету;
- входные строки параметризуются или экранируются;
- таблицы используют уникальные индексы;
- запросы выполняются асинхронно;
- ошибки записываются без секретных данных.
Сессии
- при подключении данные слота очищаются;
- каждая сессия имеет уникальный номер;
- callbacks проверяют session ID;
- до входа игровые функции заблокированы;
- вход имеет ограничение времени;
- повторные ответы диалога игнорируются;
- игрок помечается вошедшим только после загрузки.
Защита от перебора
- ведётся счётчик неудачных попыток;
- применяется временная задержка;
- учитываются аккаунт и IP;
- нет простой постоянной блокировки;
- подозрительные события записываются;
- успешный вход сбрасывает счётчик.
Администрация
- права загружаются из базы;
- нет скрытой выдачи по нику;
- RCON не выдаётся персоналу;
- изменение уровня записывается;
- старые администраторы удаляются;
- восстановление аккаунтов контролируется.
Резервное копирование аккаунтов
Создание дампа
mysqldump \
--single-transaction \
-u crmp_user \
-p \
crmp_server \
> crmp-accounts.sql
Сжатый архив с датой
mysqldump \
--single-transaction \
-u crmp_user \
-p \
crmp_server \
| gzip \
> crmp-accounts-$(date +%F).sql.gz
Перед изменением системы входа сохраните
- таблицы аккаунтов;
- таблицы персонажей;
- игровой AMX;
- исходный PWN;
- MySQL-плагин;
- include-файл;
- bcrypt или Argon2-плагин;
- конфигурацию подключения;
- миграционные SQL-файлы.
Копии необходимо хранить
- на отдельном сервере;
- в защищённом облачном хранилище;
- на компьютере владельца;
- в зашифрованном архиве;
- отдельно от рабочей базы.
Проверяйте восстановление
Создайте тестовую базу, импортируйте дамп и убедитесь, что существующий аккаунт может пройти авторизацию.
Частые вопросы по регистрации и авторизации CRMP
Где хранить аккаунты игроков?
Обычно они хранятся в отдельной таблице MySQL. Простые старые моды могут использовать INI или SQLite.
Можно ли хранить пароль открытым текстом?
Нет. В базе должен находиться только результат безопасного адаптивного хеширования.
Какой алгоритм использовать?
Предпочтителен Argon2id. При отсутствии совместимой реализации может использоваться bcrypt или другой проверенный адаптивный алгоритм.
Можно ли использовать SHA256_PassHash?
Для новой системы лучше использовать современный алгоритм хранения паролей. В актуальной документации open.mp эта функция отмечена как устаревшая.
Нужно ли отдельно хранить salt?
Современные библиотеки обычно включают уникальный salt и параметры в итоговую строку хеша.
Можно ли расшифровать сохранённый пароль?
Нет. При входе проверяется соответствие введённого пароля сохранённому хешу.
Куда устанавливать bcrypt-плагин?
Нативный файл помещается в plugins, а соответствующий include — в pawno/include.
Почему bcrypt показывает Failed?
Проверьте операционную систему, расширение DLL или SO, архитектуру и системные зависимости.
Как проверить существование аккаунта?
Выполнить асинхронный SELECT по имени с параметром или корректным экранированием.
Нужно ли выполнять SELECT перед INSERT?
Для интерфейса — да, но окончательную защиту от повторной записи должен обеспечивать уникальный индекс.
Почему аккаунт создаётся два раза?
Проверьте уникальный индекс, состояние авторизации и повторную обработку диалога.
Почему любой пароль подходит?
Проверьте результат callback хеширующего плагина и момент установки статуса авторизации.
Почему правильный пароль не подходит?
Хеш может быть обрезан, создан другой версией алгоритма или неправильно передан в функцию проверки.
Какой размер нужен для password_hash?
Для универсального строкового формата обычно используют VARCHAR(255).
Нужно ли сохранять пароль в Pawn-массиве?
Только кратковременно, если этого требует API плагина. После завершения операции временную строку следует очистить.
Можно ли использовать mysql_tquery без экранирования?
Нет. Асинхронность не защищает запрос от SQL-инъекции.
Как защититься от перебора?
Считать ошибки, увеличивать задержку и временно ограничивать вход по аккаунту и IP.
Нужно ли блокировать аккаунт навсегда?
Обычно нет. Постоянную блокировку можно использовать для намеренного отказа в доступе владельцу.
Как восстановить пароль?
Через одноразовый ограниченный по времени токен или контролируемую процедуру подтверждения владения аккаунтом.
Можно ли использовать секретные вопросы?
Это слабый способ восстановления: ответы часто легко узнать или подобрать.
Почему данные игрока загружаются другому?
Проверьте session ID асинхронных callbacks и повторное использование playerid.
Когда считать игрока авторизованным?
Только после успешной проверки пароля и загрузки обязательных данных.
Когда сохранять аккаунт?
Периодически, при критических операциях, при штатном выходе и перед обслуживанием сервера.
Можно ли сохранять по игровому имени?
Лучше использовать постоянный числовой ID аккаунта или персонажа.
Где хранить административный уровень?
В серверной базе данных. Не выдавайте права простой проверкой ника в исходном коде.
Нужно ли записывать IP?
Он полезен для безопасности и диагностики, но не должен считаться надёжным доказательством личности.
Как проверить систему перед открытием?
Протестировать регистрацию, правильный и неправильный пароль, перебор, повторные подключения, сохранение и восстановление базы.
Официальные и технические источники
Заключение
Система регистрации CRMP начинается с проверки имени игрока в MySQL. Если запись отсутствует, сервер предлагает создать аккаунт; если запись существует — запросить пароль.
Пароли нельзя сохранять открытым текстом или в обратимо зашифрованном виде. Для новой системы следует использовать современный адаптивный алгоритм и совместимый серверный плагин.
SQL-запросы должны использовать параметры или обязательное экранирование всех входных строк. Асинхронное выполнение защищает игровой поток от ожидания, но не предотвращает SQL-инъекции.
Каждый callback необходимо связывать с конкретной игровой сессией. Проверки одного playerid недостаточно, поскольку после отключения этот номер может получить другой пользователь.
Игрок считается авторизованным только после успешной проверки пароля и загрузки обязательных данных. До этого момента команды и игровые функции должны быть недоступны.
Для защиты от перебора используйте счётчик неудачных попыток, временные задержки и журналирование подозрительных входов. Не применяйте простую постоянную блокировку после нескольких ошибок.
Сохраняйте данные периодически, при критических операциях и штатном выходе. Регулярно создавайте дампы базы и проверяйте их восстановление на отдельной тестовой копии.
Подключение и импорт базы данных:
Установка серверных расширений:
Настройка игрового режима:
Комментарии к инструкции
Обсудите решение, задайте вопрос или дополните инструкцию