Firebase如何保护数字数据不被用户操纵,例如游戏分数

Firebase如何保护数字数据不被用户操纵,例如游戏分数,firebase,firebase-realtime-database,firebase-security,Firebase,Firebase Realtime Database,Firebase Security,我正在用Firebase开发一个多人游戏。每场比赛结束后,玩家的得分都会记录在firebase中,而且玩家的总得分字段也会用新的总得分更新。 我的问题:是否可以仅使用firebase安全规则来保护playerTotalScore字段,防止用户任意操纵?如果是,怎么做 我详细阅读了firebase网站上的firebase安全信息。虽然我知道可以在安全规则中实现一些复杂的逻辑(将一个数字增加一个给定的数量,如此,或者只插入字段(”.write):“!data.exists()”),在这种情况下,所有

我正在用Firebase开发一个多人游戏。每场比赛结束后,玩家的得分都会记录在firebase中,而且玩家的总得分字段也会用新的总得分更新。 我的问题:是否可以仅使用firebase安全规则来保护playerTotalScore字段,防止用户任意操纵?如果是,怎么做

我详细阅读了firebase网站上的firebase安全信息。虽然我知道可以在安全规则中实现一些复杂的逻辑(将一个数字增加一个给定的数量,如此,或者只插入字段(
”.write):“!data.exists()”
),在这种情况下,所有信息似乎都没有帮助。仅递增规则是不够的,因为分数可以通过多次递增来操纵。仅插入似乎是totalScore的一个选项,因为它在每场比赛后都会更新

更新 按照加藤的要求,下面是具体的用例

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

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

gameRef.child('players').child(UserId).child('score').set(gameScore)
leaderboardRef.child(userId).setWithPriority({userName:userName, totalScore:totalScore}, totalScore)
游戏结束后,玩家的总分(所有已玩游戏)计算为
totalScore=totalScore+gameScore
,然后使用以下语句在Firebase中更新玩家的总分:

gameRef.child('players').child(UserId).child('score').set(gameScore)
leaderboardRef.child(userId).setWithPriority({userName:userName, totalScore:totalScore}, totalScore)
更新2:加藤要求的数据结构 这是我目前拥有的具体结构。这不是一成不变的,因此我愿意根据推荐的方法对其进行任何必要的更改,以保护数据

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

<firebase_root>/app/games/<gameId>/players/<userId>/score/
<firebase_root>/app/leaderboard/<userId>/totalScore/
score和totalScore都是数字整数值。
这就是我所能想到的当前数据结构的全部细节。

使用规则来防范无效值将是一件棘手的事情。由于您授予用户编写值的权限,他们还可以对您的代码进行反向工程,并编写您不希望看到的值。您可以做很多事情来让黑客的工作更加困难,但是总会有人能够解决这个问题。也就是说:你可以做一些简单的事情,让黑客的事情变得不那么琐碎

您可以轻松地记录/存储足够的游戏信息,以便以后确定游戏是否合法

例如,在我做的一个打字游戏中,我不仅存储了玩家的最终分数,还存储了他们按下的每个键以及他们按下的时间

https://<my>.firebaseio.com/highscores/game_1_time_15/puf
  keystrokes: "[[747,'e'],[827,'i'],[971,'t'],[1036,'h']...[14880,'e']]"
  score: 61
https://.firebaseio.com/highscores/game_1_time_15/puf
击键:“[[747,'e'],[827,'i'],[971,'t'],[1036,'h']…[14880,'e']”
分数:61
所以在游戏进行到747秒时,我输入了一个e,然后是I,t,h等等,直到最后14.8秒后我按下了e

使用这些值,我可以检查按下的键是否真的导致得分为
61
。我也可以重放游戏,或者对其进行一些分析,看看是否是真人在按键。如果时间戳是
100
200
300
,等等,你会非常怀疑(尽管我创建了一些机器人,它们的类型正好是在这样的时间间隔内)

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


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

我有一个想法。-因为这是一个多人游戏,你将在一个特定的游戏中有多个玩家。这意味着
游戏结束后的每个玩家将更新部分和总分数

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

黑客必须想出一些精心设计的计划,包括控制多个帐户和同步攻击

编辑:
…我可以看到进一步的问题-第一个更新的玩家怎么办?这可以通过意向来完成。因此,首先所有玩家都编写一个
意向来写分数
部分分数将在哪里,一旦到处都有一些值,他们就可以清楚地写下实际分数。

从技术上讲,你的问题是如何完成e这是使用安全规则实现的,但由于这有点复杂,并且没有排除任何其他可能性,因此我也将在这里处理其中的一些问题

我将做大量的假设,因为回答这个问题实际上需要一组完全指定的规则,需要遵循这些规则,实际上是实现整个应用程序的问题(增加分数是游戏逻辑规则的结果,而不是简单的数学问题)

在客户端计算总分 也许解决这个难题最简单的办法就是不求总分,只需抓取玩家列表,手动求总即可

当这可能有用时:

  • 球员名单上有几百人或更少
  • 播放器数据适当小(不是每个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\u PROCESS'”
)这可能是最简单的解决方案,也最容易维护,但缺点是需要另一个工作部件

当这可能有用时:

  • 您可以启动Heroku服务或将.js文件部署到webscript.io
  • 额外的
    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;
            });
        }
    }
    
    /games/$gameid/users/$userid/score
    /leaderboard_audit/$userid/$gameid/score
    /leaderboard/$userid = { last_game: $gameid, score: <int> }
    
    {
        "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()
                        "
                    }
                }
            }
        }
    }