Защита: почему сервер не верит клиенту
Писать серверный код, который нельзя обмануть сообщением с клиента.
Теория
Код на устройстве игрока полностью в его власти: значения переменных, вызовы функций, содержимое событий. Это не повод бояться — это условие задачи. Игра защищена ровно настолько, насколько мало она доверяет клиенту.
Практика сводится к трём вещам. Проверяй тип и содержание каждого аргумента. Держи справочные данные — цены, характеристики, права — на сервере. Ограничивай частоту: даже правильный запрос, отправленный тысячу раз в секунду, ломает экономику игры.
Этот урок — про защиту своей игры. Как устроены программы, которые вмешиваются в чужую, и чем это заканчивается для аккаунта, разобрано в разделе безопасности портала.
- typeof проверяет тип, отдельная проверка — диапазон значения
- Никогда не принимай от клиента цену, урон или количество
- Ограничение частоты по UserId — простая таблица с временем последнего вызова
- Проверка в LocalScript — удобство, а не защита: сервер обязан повторить её
Пример
Серверный обработчик с проверкой аргументов и ограничением частоты.
local ReplicatedStorage = game:GetService("ReplicatedStorage")local useAbility = ReplicatedStorage:WaitForChild("UseAbility")local COOLDOWN = 3local ABILITIES = { dash = true, heal = true }local lastUse = {}useAbility.OnServerEvent:Connect(function(player, name)if typeof(name) ~= "string" or not ABILITIES[name] thenreturnendlocal now = os.clock()local previous = lastUse[player.UserId]if previous and now - previous < COOLDOWN thenreturnendlastUse[player.UserId] = nowprint(player.Name .. " применил " .. name)end)
Список способностей и время отката живут на сервере — клиент их не задаёт.
Задача
Перепиши небезопасный обработчик: сейчас он принимает от клиента количество монет и начисляет его как есть. Сделай так, чтобы количество определял сервер, а частота была ограничена.
- Награда — константа на сервере, а не аргумент события
- Не забудь убрать запись игрока из таблицы при выходе, иначе она будет расти
Показать решение
local Players = game:GetService("Players")local ReplicatedStorage = game:GetService("ReplicatedStorage")local claim = ReplicatedStorage:WaitForChild("ClaimCoins")local REWARD = 10local COOLDOWN = 60local lastClaim = {}claim.OnServerEvent:Connect(function(player)local now = os.clock()if lastClaim[player.UserId] and now - lastClaim[player.UserId] < COOLDOWN thenreturnendlocal stats = player:FindFirstChild("leaderstats")local coins = stats and stats:FindFirstChild("Coins")if not coins thenreturnendlastClaim[player.UserId] = nowcoins.Value += REWARDend)Players.PlayerRemoving:Connect(function(player)lastClaim[player.UserId] = nilend)
Проверь себя
Клиент прислал в событие количество монет. Что должен сделать сервер?
Показать ответ
Сумма награды — решение сервера. Аргумент от клиента здесь ничего не значит.
Зачем ограничивать частоту вызова события?
Показать ответ
Без ограничения частоты честная механика превращается в бесконечный источник наград.