最可靠的&;PHP中防止种族状况的安全方法


Most reliable & safe method of preventing race conditions in PHP

我需要在PHP中使用互斥或信号量,这让我很害怕。为了澄清,我不怕写正确同步的无死锁代码,也不怕并发编程的危险,而是害怕PHP处理边缘情况的能力。

快速背景:编写一个位于用户和第三方信用卡网关之间的信用卡处理程序界面。需要防止重复请求,并且已经有了一个可以工作的系统,但如果用户每隔几毫秒点击提交(启用了JS,所以我不能为他们禁用按钮),就会出现竞争条件,我的PHP脚本没有意识到已经发出了重复请求。需要一个信号量/互斥量,这样我就可以确保每个唯一事务只通过一个成功的请求。

我通过PHP-FPM在nginx后面运行PHP,在多核Linux机器上有多个进程。我想确定

  1. 信号量在所有php-fpm进程之间以及在所有内核(i686内核)之间共享
  2. php-fpm在持有互斥/信号量时处理php进程崩溃,并相应地释放它
  3. php-fpm在持有互斥/信号量时处理会话中止,并相应地释放它

是的,我知道。非常基本的问题,如果认为任何其他软件都不存在合适的解决方案,那将是愚蠢的。但这是PHP,它的构建肯定没有考虑到并发性,它经常崩溃(取决于您加载的扩展),并且处于不稳定的环境中(PHP-FPM和web上)。

关于(1),我假设如果PHP使用POSIX函数,那么这两个条件在SMP i686机器上都成立。至于(2),我从简要浏览文档中看到,有一个参数决定了这种行为(尽管我不明白为什么PHP不释放互斥体是因为会话被终止了)。但是(3)是我主要关心的问题,我不知道假设php-fpm为我正确地处理所有边缘情况是否安全。我(显然)从不希望出现死锁,但我不确定我是否可以相信php永远不会让我的代码处于无法获得互斥的状态,因为获取它的会话要么被优雅地终止,要么被不礼貌地终止。

我曾考虑过使用MySQL LOCK TABLES方法,但还有更多的疑问,因为虽然我更信任MySQL锁而不是PHP锁,但我担心如果PHP在持有MySQL会话锁时中止请求(*out*崩溃),MySQL可能会锁定表(尤其是因为我很容易想象会导致这种情况发生的代码)。

老实说,我最喜欢的是一个非常基本的C扩展,在那里我可以准确地看到正在进行的POSIX调用,以及使用什么参数来确保我想要的确切行为。。但我并不期待写那个代码。

有人想分享关于PHP的任何与并发相关的最佳实践吗?

事实上,我认为无论什么解决方案,都不需要复杂的互斥体/信号量。

存储在PHP $_SESSION中的表单键就是您所需要的全部。作为一个很好的副作用,这种方法还可以保护你的形态免受CSRF的攻击。

在PHP中,通过获取POSIX flock()来锁定会话,而PHP的session_start()则等待用户会话释放。您只需要unset()第一个有效请求的表单密钥。第二个请求必须等待,直到第一个请求释放会话。

然而,当在涉及多个主机的(不是基于会话或源ip的)负载平衡场景中运行时,事情会变得更加复杂。对于这种情况,我相信你会在这篇伟大的论文中找到一个有价值的解决方案:http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/

我用下面的演示重现了您的用例。只需将此文件放到您的Web服务器上并进行测试:

<?php
session_start();
if (isset($_REQUEST['do_stuff'])) {
  // do stuff
  if ($_REQUEST['uniquehash'] == $_SESSION['uniquehash']) {
    echo "valid, doing stuff now ... "; flush();
    // delete formkey from session
    unset($_SESSION['uniquehash']);
    // release session early - after committing the session data is read-only
    session_write_close();
    sleep(20);  
    echo "stuff done!";
  }
  else {
    echo "nope, {$_REQUEST['uniquehash']} is invalid.";
  }     
}
else {
  // show form with formkey
  $_SESSION['uniquehash'] = md5("foo".microtime().rand(1,999999));
?>
<html>
<head><title>session race condition example</title></head>
<body>
  <form method="POST">
    <input type="hidden" name="PHPSESSID" value="<?=session_id()?>">
    <input type="text" name="uniquehash" 
      value="<?= $_SESSION['uniquehash'] ?>">
    <input type="submit" name="do_stuff" value="Do stuff!">
  </form>
</body>
</html>
<?php } ?>

这是一个有趣的问题,但您没有任何数据或代码可以显示。

在80%的情况下,如果你遵循关于阻止用户多次提交表单的标准程序和实践,那么由于PHP本身而发生任何恶劣事件的可能性几乎为零,这几乎适用于其他所有设置,而不仅仅是PHP。

如果你是20%,并且你的环境需要它,那么一种选择是使用消息队列,我相信你很熟悉。同样,这种观点是语言不可知论的。与语言无关。这一切都与数据如何移动有关。

您可以将随机哈希存储在会话数据中的数组中,也可以将该哈希打印为隐藏的表单输入值。当收到请求时,如果会话数组中存在隐藏的哈希值,则可以从会话中删除该哈希并处理表单,否则不处理。

这应该可以防止重复的表单提交,并有助于防止csrf攻击。

如果问题只出现在相隔几毫秒按下一个按钮时,软件解除绑定不起作用吗?比如在会话变量中节省按下按钮的时间,而不允许再按下一秒钟?就在我早上喝咖啡之前。干杯

为了防止代码中的会话竞争条件,我所做的是在会话中存储数据的最后一次操作之后。我使用PHP函数session_write_close()注意,如果您使用的是PHP 7,则需要在PHP.ini中禁用默认输出缓冲。如果您有耗时的操作,最好在调用session_write_close(。

我希望它能帮助别人,对我来说它救了我的命:)