Firebase如何保护用户操纵数字数据,例如。比赛得分

时间:2014-10-08 05:50:13

标签: firebase firebase-realtime-database firebase-security

我正在使用Firebase开发多人游戏。每场比赛后,玩家得分记录在火力基础中,并且玩家总得分字段也会使用新总计进行更新。 我的问题:是否有可能使用firebase安全规则来保护playerTotalScore字段免受用户的任意操纵?如果是这样,怎么样?

我已详细阅读了firebase网站上的firebase安全信息。虽然我理解可以在安全规则中实现一些复杂的逻辑(按照这个gist增加一个给定的数量,或者只使用字段插入(".write": "!data.exists()"),但没有一个在这种情况下,信息似乎有所帮助。仅增量规则是不够的,因为分数可以通过多次递增来操纵。仅插入似乎是totalScore的一个选项,因为在每次游戏后都会更新。

更新

根据加藤的要求,这是具体的用例。

我正在开发的游戏是一个问答游戏,其中玩家回答问题,并且玩家得分实时显示。

在游戏过程中,通过以下声明在每个问题之后更新该特定游戏的得分:

gameRef.child('players').child(UserId).child('score').set(gameScore)

游戏结束后,玩家的总分数(所有游戏玩法)计算为totalScore=totalScore+gameScore,然后使用以下语句在Firebase中更新玩家总分:

leaderboardRef.child(userId).setWithPriority({userName:userName, totalScore:totalScore}, totalScore)

更新2:加藤要求的数据结构

这是我目前的具体结构。这不是一成不变的,所以我愿意根据建议的方法来保护数据,无论如何需要更改它。

用户(玩家)玩的每个游戏的得分存储在以下结构中

<firebase_root>/app/games/<gameId>/players/<userId>/score/

<gameId>是由于调用firebase push()方法而生成的firebase密钥。 <UserId>是firebase simplelogin uid。

每个用户(玩家)的总分数(所有游戏的所有分数的总和)存储在以下数据结构中

<firebase_root>/app/leaderboard/<userId>/totalScore/

totalScore的排行榜数据是使用totalScore作为优先级设置的,用于查询目的

leaderboardRef.child(userId).setWithPriority({userName:userName, totalScore:totalScore}, totalScore)

得分和总分数都是数字整数值。 这就是我能想到的当前数据结构的所有细节。

3 个答案:

答案 0 :(得分:12)

您的问题在技术上是如何使用安全规则来完成此操作,但因为它有点XY problem,并且没有排除任何其他可能性,我也会在这里解决其中的一些问题。

我会做出很多假设,因为回答这个问题实际上需要一套完整指定的规则需要遵循,而且实际上是一个实现整个应用程序的问题(增加分数是由于游戏逻辑规则,而不是简单的数学问题。)

客户总分

对这个难题最简单的答案可能就是没有总得分。只需抓住玩家列表并手动累计。

如果这可能有用:

  • 玩家列表数百或更少
  • 玩家数据适当小(不是每个500k)

怎么做:

var ref = new Firebase(URL);
function getTotalScore(gameId, callback) {
   ref.child('app/games/' + gameId + '/players').once('value', function(playerListSnap) {
      var total = 0;
      playerListSnap.forEach(function(playerSnap) {
         var data = playerSnap.val();
         total += data.totalScore || 0;
      });
      callback(gameId, total);
   });
}

使用特权工作者更新分数

一种非常复杂且简单的方法(因为它只需要将安全规则设置为类似".write": "auth.uid === 'SERVER_PROCESS'")将使用仅监视游戏并累积总数的服务器进程。这可能是最简单,最容易维护的解决方案,但却有需要另一个工作部分的缺点。

如果这可能有用:

  • 您可以启动Heroku服务或将.js文件部署到webscript.io
  • $ 5- $ 30范围内的额外每月订阅不是交易破坏者

怎么做:

显然,这涉及大量的应用程序设计,并且必须实现各种级别。让我们只关注结束游戏并统计排行榜,因为这是一个很好的例子。

首先将评分代码拆分为自己的路径,例如

/scores_entries/$gameid/$scoreid = < player: ..., score: ... >
/game_scores/$gameid/$playerid = <integer>

现在监控游戏以了解它们何时关闭:

var rootRef = new Firebase(URL);
var gamesRef = rootRef.child('app/games');
var lbRef = rootRef.child('leaderboards');

gamesRef.on('child_added', watchGame);
gamesRef.child('app/games').on('child_remove', unwatchGame);

function watchGame(snap) {
    snap.ref().child('status').on('value', gameStatusChanged);
}

function unwatchGame(snap) {
    snap.ref().child('status').off('value', gameStatusChanged);
}

function gameStatusChanged(snap) {
    if( snap.val() === 'CLOSED' ) {
        unwatchGame(snap);
        calculateScores(snap.name());
    }
}

function calculateScores(gameId) {
    gamesRef.child(gameId).child('users').once('value', function(snap) {
        var userScores = {};
        snap.forEach(function(ss) {
            var score = ss.val() || 0;
            userScores[ss.name()] = score;
        });
        updateLeaderboards(userScores);
    });
}

function updateLeaderboards(userScores) {
    for(var userId in userScores) {
        var score = userScores[userId];
        lbRef.child(userId).transaction(function(currentValue) {
            return (currentValue||0) + score;
        });
    }
}

使用审计路径和安全规则

当然,这将是现有选择中最复杂和最困难的。

如果这可能有用:

  • 当我们拒绝使用涉及服务器进程的任何其他策略时
  • 当担心球员作弊时非常担心
  • 当我们有更多的额外时间来燃烧时

显然,我对这种方法有偏见。主要是因为很难做到正确,需要大量的能源,可以用小额的货币投资取而代之。

要实现这一点,需要仔细检查每个单独的写入请求。有几个明显的要点可以保证(可能更多):

  1. 编写包含分数增量的任何游戏事件
  2. 为每位用户编写游戏总数
  3. 将游戏总数写入排行榜
  4. 编写每个审核记录
  5. 确保无法在飞行中创建和修改多余的游戏以提高分数
  6. 以下是确保这些要点的一些基本原则:

    • 使用审核跟踪,用户只能添加(不更新或删除)条目
    • 验证每个审核条目的优先级是否等于当前时间戳
    • 验证每个审核条目是否包含根据当前游戏状态的有效数据
    • 在尝试增加运行总计时使用审核条目

    举个例子,我们可以安全地更新排行榜。我们假设如下:

    • 用户在游戏中的得分有效
    • 用户创建了一个审核条目,例如,leaderboard_audit / $ userid / $ gameid,当前时间戳为优先级,分数为值
    • 每个用户记录提前存在于排行榜中
    • 只有用户可以更新自己的分数

    所以这是我们假设的数据结构:

    /games/$gameid/users/$userid/score
    /leaderboard_audit/$userid/$gameid/score
    /leaderboard/$userid = { last_game: $gameid, score: <int> }
    

    以下是我们的逻辑运作方式:

    1. 游戏分数设为/games/$gameid/users/$userid/score
    2. /leaderboard_audit/$userid/games_played/$gameid
    3. 创建审核记录
    4. /leaderboard_audit/$userid/last_game的值已更新为与$gameid
    5. 相匹配
    6. 排行榜的更新金额完全等于last_game的审核记录
    7. 以下是实际规则:

      {
          "rules": {
              "leaderboard_audit": {
                  "$userid": {
                      "$gameid": {
                         // newData.exists() ensures records cannot be deleted
                          ".write": "auth.uid === $userid && newData.exists()",
      
                          ".validate": "
                              // can only create new records
                              !data.exists()
                              // references a valid game
                              && root.child('games/' + $gameid).exists()
                              // has the correct score as the value
                              && newData.val() === root.child('games/' + $gameid + '/users/' + auth.uid + '/score').val()
                              // has a priority equal to the current timestamp
                              && newData.getPriority() === now
                              // is created after the previous last_game or there isn't a last_game
                              (
                                  !root.child('leaderboard/' + auth.uid + '/last_game').exists() || 
                                  newData.getPriority() > data.parent().child(root.child('leaderboard/' + auth.uid + '/last_game').val()).getPriority()
                              )
      
                          "
                      }
                  }
              },
              "leaderboard": {
                  "$userid": {
                      ".write": "auth.uid === $userid && newData.exists()",
                      ".validate": "newData.hasChildren(['last_game', 'score'])",
                      "last_game": {
                          ".validate": "
                              // must match the last_game entry
                              newData.val() === root.child('leaderboard_audit/' + auth.uid + '/last_game').val()
                              // must not be a duplicate
                              newData.val() !== data.val()
                              // must be a game created after the current last_game timestamp
                              (
                                  !data.exists() ||
                                  root.child('leaderboard_audit/' + auth.uid + '/' + data.val()).getPriority() 
                                  < root.child('leaderboard_audit/' + auth.uid + '/' + newData.val()).getPriority()
                              )
                          "
                      },
                      "score": {
                          ".validate": "
                              // new score is equal to the old score plus the last_game's score
                              newData.val() === data.val() + 
                              root.child('games/' + newData.parent().child('last_game').val() + '/users/' + auth.uid + '/score').val()
                          "
                      }
                  }
              }
          }
      }
      

答案 1 :(得分:3)

使用规则防止无效值是很棘手的。由于您授予用户写入值的权限,因此他们还可以对您的代码进行反向工程并编写您不会看到的值。你可以做很多事情来使黑客的工作更加困难,但总会有人能够解决这个问题。这就是说:你可以采取一些简单的措施来为黑客提供一些不那么重要的东西。

您可以轻松做的事情是记录/存储有关游戏玩法的足够信息,以便您以后确定它是否合法。

因此,例如在我所做的打字游戏中,我不仅存储了玩家的最终得分,还记录了他们按下的每个按键以及按下它们时的每个按键。

https://<my>.firebaseio.com/highscores/game_1_time_15/puf
  keystrokes: "[[747,'e'],[827,'i'],[971,'t'],[1036,'h']...[14880,'e']]"
  score: 61

所以在747ms进入游戏时,我键入了 e 然后 i t h 等等直到最后14.8秒后,我按下 e

使用这些值,我可以检查按下的键是否确实导致得分为61。我也可以重播游戏,或者对它进行一些分析,看看它是否真的是人类按下按键。如果时间戳是100200300等,那么您会非常怀疑(尽管我创建了一些类似于此类间隔的机器人)。

它当然还不能保证,但它至少是ref.child('score').set(10000000)黑客的第一个绊脚石。

我从John Resig的Deap Leap那里得到了这个想法,但是我无法找到他描述它的页面。

答案 2 :(得分:3)

我有个主意。 - 因为这是一个多人游戏,你将在一个特定的游戏中拥有多个玩家。这意味着game over消息之后的每个玩家都将更新部分和总分。

在安全规则中,您可以检查对手是否已写入有关同一游戏的部分值。 - 那将是只读访问。或者你可以检查所有对手的部分值是否给出了所需的总数等。

黑客必须提出一些精心策划的计划,包括控制多个帐户并同步攻击。

编辑: ......我可以看到更进一步的问题 - 第一个要更新的玩家怎么样?这可以通过意图来完成。因此,首先所有玩家都会写一个intent to write score,其中部分得分将会出现,一旦到处都有一些值,他们就会清楚地写出实际得分。