Урок 15 из 15

Защита: почему сервер не верит клиенту

Писать серверный код, который нельзя обмануть сообщением с клиента.

Теория

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

Практика сводится к трём вещам. Проверяй тип и содержание каждого аргумента. Держи справочные данные — цены, характеристики, права — на сервере. Ограничивай частоту: даже правильный запрос, отправленный тысячу раз в секунду, ломает экономику игры.

Этот урок — про защиту своей игры. Как устроены программы, которые вмешиваются в чужую, и чем это заканчивается для аккаунта, разобрано в разделе безопасности портала.

  • typeof проверяет тип, отдельная проверка — диапазон значения
  • Никогда не принимай от клиента цену, урон или количество
  • Ограничение частоты по UserId — простая таблица с временем последнего вызова
  • Проверка в LocalScript — удобство, а не защита: сервер обязан повторить её

Пример

Серверный обработчик с проверкой аргументов и ограничением частоты.

ServerScriptService/UseAbility.lua
  1. local ReplicatedStorage = game:GetService("ReplicatedStorage")
  2. local useAbility = ReplicatedStorage:WaitForChild("UseAbility")
  3.  
  4. local COOLDOWN = 3
  5. local ABILITIES = { dash = true, heal = true }
  6.  
  7. local lastUse = {}
  8.  
  9. useAbility.OnServerEvent:Connect(function(player, name)
  10. if typeof(name) ~= "string" or not ABILITIES[name] then
  11. return
  12. end
  13.  
  14. local now = os.clock()
  15. local previous = lastUse[player.UserId]
  16.  
  17. if previous and now - previous < COOLDOWN then
  18. return
  19. end
  20.  
  21. lastUse[player.UserId] = now
  22. print(player.Name .. " применил " .. name)
  23. end)

Список способностей и время отката живут на сервере — клиент их не задаёт.

Задача

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

  • Награда — константа на сервере, а не аргумент события
  • Не забудь убрать запись игрока из таблицы при выходе, иначе она будет расти
Показать решение
ServerScriptService/Solution.lua
  1. local Players = game:GetService("Players")
  2. local ReplicatedStorage = game:GetService("ReplicatedStorage")
  3. local claim = ReplicatedStorage:WaitForChild("ClaimCoins")
  4.  
  5. local REWARD = 10
  6. local COOLDOWN = 60
  7.  
  8. local lastClaim = {}
  9.  
  10. claim.OnServerEvent:Connect(function(player)
  11. local now = os.clock()
  12.  
  13. if lastClaim[player.UserId] and now - lastClaim[player.UserId] < COOLDOWN then
  14. return
  15. end
  16.  
  17. local stats = player:FindFirstChild("leaderstats")
  18. local coins = stats and stats:FindFirstChild("Coins")
  19.  
  20. if not coins then
  21. return
  22. end
  23.  
  24. lastClaim[player.UserId] = now
  25. coins.Value += REWARD
  26. end)
  27.  
  28. Players.PlayerRemoving:Connect(function(player)
  29. lastClaim[player.UserId] = nil
  30. end)

Проверь себя

Клиент прислал в событие количество монет. Что должен сделать сервер?

Показать ответ

Сумма награды — решение сервера. Аргумент от клиента здесь ничего не значит.

Зачем ограничивать частоту вызова события?

Показать ответ

Без ограничения частоты честная механика превращается в бесконечный источник наград.