如何在增加Firebase的投票次数时防止重复投票?

时间:2014-04-09 18:55:29

标签: firebase atomic atomicity firebase-security

我正在Firebase中构建实时投票。每个投票都存储在列表字段中。为了防止必须将每个投票都下载到客户端以便计算它们,我会在计数器字段中为每个选项缓存标签。

poll1
    counts
        choice1: 5
        choice2: 2
        choice3: 10
        choice4: 252
    voters
        uid1 : choice1
        uid6 : choice3
        uid25: choice2
        uid31: choice1

我目前正在使用以下交易更新计数器:

var fireCountPush = new Firebase(self.fireCountUrl+node+id);
fireCountPush.transaction(function(current_value) {
    return current_value + 1;
}, function(error, committed, snapshot) {
    if(committed) {
        var fireVote = new Firebase(self.fireVotesUrl);
        fireVote.child(self.user.uid).set(id);
    }
});

但是,我希望以原子方式将用户添加到选民列表中,理想情况是在同一事务中。不幸的是,我现在拥有它的方式,我必须在事务提交成功后添加用户。这是一个巨大的安全问题,因为可以通过编辑脚本在浏览器中轻松禁用它。

有没有办法更新计数器将用户添加到选民列表中而无需在交易中下载整个对象?

2 个答案:

答案 0 :(得分:7)

这里最简单的答案是让中立方(服务器脚本)监控选民名单并增加计数器。然后只需要确保用户通过他们的uid添加自己,并且只能这样做一次。

我确信还有一些很好的方法可以完全遵循安全规则。不幸的是,我不是那么出色,但如果你真的想要一个仅限客户解决方案的痛苦,那么你可以改进这种蛮力方法。

计划:

  • 强制用户首先编写审核记录
  • 强迫他们将自己的名字添加到选民名单
  • 允许他们在这两个记录都存在且与投票号
  • 匹配时更新计数器

架构:

/audit/<$x>/<$user_id>
/voters/$user_id/<$x>
/total/<$x>

我们阻止用户修改审核/如果他们已经投票(选民/ $ user_id存在),或者审核记录已经存在(某人已经声明了该计数),或者投票没有增加一个:

"audit": {
  "$x": {
     ".write": "newData.exists() && !data.exists()", // no delete, no overwrite
     ".validate": "!root.child('voters/'+auth.uid).exists() && $x === root.child('total')+1"
  }
}

您会在交易中更新audit,主要是尝试&#34;声明&#34;每次增加直到成功并取消事务(通过返回undefined)任何时候要添加的记录不为空(某人已经声明了它)。这将为您提供唯一的投票号。

为了防止任何有趣的事情,我们存储一份选民名单,强制每个选民只能写入审计/一次。如果我以前从未投票过,并且只有用我唯一的投票号创建审计记录,我才能写信给选民:

"voters": {
  "$user_id": {
     ".write": "newData.exists() && !data.exists()", // no delete, no replace
     ".validate": "newData.isNumber() && root.child('audit/'+newData.val()).val() === $user_id"
  }
}

最后,但并非最不重要的是,我们更新计数器以匹配我们声称的投票ID。它必须与我的投票号匹配,而且可能只会增加。这可以防止一个用户创建审计记录和选民记录的竞争条件,但是在完成我的三个步骤之前,其他人已经增加了总数。

"total": {
  ".write": "newData.exists()", // no delete
  ".validate": "newData.isNumber() && newData.val() === root.child('audit/'+auth.uid).val() && newData.val() > data.val()"
}

更新总计,如添加初始审核记录,将在交易中完成。如果总计的当前值大于我指定的投票号,那么我只是取消与undefined的交易,因为其他人已经在我之后投票并更新了更高。不是问题。

答案 1 :(得分:0)

继我的第二次评论之后 - 为什么要单独计算?

在您推送的UniqueId记录中保持计数(每个新记录的增量) - 而不是观察计数的变化,在UniqeID记录上观察限制为1的“add_child”。这将返回目前的总数。

/用户ID / =计数

完成工作 - 一次更新。

修改

如果你还在考虑它......我会给你一个设计模式,以减轻其缺乏原子性......

'喷涂和模板更新'

我们的验证无法推动更新,但知道更新应该是什么(计数应该增加一个而且只有一个) - 这是被动模板,只是过滤掉不应该通过的内容。 / p>

我们有一个可以推动更新的客户端,但不能确定更新应该是什么。它希望将计数更新一次,但计数自上次读取后可能会增加!这将无法通过验证,它将不得不再次尝试,但同样可能发生,我们有竞争条件......

尝试一次更新并检查结果就像使用带有sencil的精细笔刷一样,这是多余的。所以让我们在循环中将更新变成'喷雾',不需要检查结果,只需调用更新 - 尝试将计数更新1,2,3,4,5,6 ...其中一个将要正确并更新!其他人将失败,因为用户已经投票(之前的更新工作),或者因为索引错误!只要喷涂的大小(尝试写入的次数)大于读取最后一个索引和喷涂更新之间的其他选民的数量,您就100%肯定会成功。

旧派纯粹主义者不喜欢这个想法,但他们将SVN强加给GIT,并且将使用SQL而不是firebase:)