假设我使用CodeIgniter/PHP和MySQL创建了一个网上银行系统,并从我的银行账户中提取了以下内容:
function withdraw($user_id, $amount) {
$amount = (int)$amount;
// make sure we have enough in the bank account
$balance = $this->db->where('user_id', $user_id)
->get('bank_account')->balance;
if ($balance < $amount) {
return false;
}
// take the money out of the bank
$this->db->where('user_id', $user_id)
->set('balance', 'balance-'.$amount, false)
->update('bank_account');
// put the money in the wallet
$this->db->where('user_id', $user_id)
->set('balance', 'balance+'.$amount, false)
->update('wallet');
return true;
}
首先,我们检查用户是否可以执行取款操作,然后从帐户中减去,然后向钱包中添加。
问题是我可以在几乎完全相同的时间发送多个请求(这基本上是微不足道的使用curl
)。每个请求都有自己的线程,所有线程都并发运行。因此,每个人都执行检查,看看我的银行里是否有足够的钱(我确实有),然后每个人都执行取款。因此,如果我开始余额为100,并且我发出两个curl
请求,同时导致提取100,那么我的钱包中最终有200,而我的银行账户中有-100,这应该是不可能的。
解决这种TOCTOU漏洞的正确"CodeIgniter"方法是什么?
我将bank_account
和wallet
表的存储引擎设置为具有事务支持的InnoDB,然后在SELECT语句中包含FOR UPDATE子句以在事务期间锁定bank_account
表。
代码应该是如下所示:
function withdraw($user_id, $amount) {
$amount = (int)$amount;
$this->db->trans_start();
$query = $this->db->query("SELECT * FROM bank_account WHERE user_id = $user_id AND balance >= $amount FOR UPDATE");
if($query->num_rows() === 0) {
$this->db->trans_complete();
return false;
}
$this->db->where('user_id', $user_id)
->set('balance', 'balance-'.$amount, false)
->update('bank_account');
$this->db->where('user_id', $user_id)
->set('balance', 'balance+'.$amount, false)
->update('wallet');
$this->db->trans_complete();
return true;
}
我要找的是表锁定。
为了使这个函数安全,我需要在函数开始时锁定表,然后在结束时释放它们:
function withdraw($user_id, $amount) {
$amount = (int)$amount;
// lock the needed tables
$this->db->query('lock tables bank_account write, wallet write');
// make sure we have enough in the bank account
$balance = $this->db->where('user_id', $user_id)
->get('bank_account')->balance;
if ($balance < $amount) {
// release the locks
$this->db->query('unlock tables');
return false;
}
// take the money out of the bank
$this->db->where('user_id', $user_id)
->set('balance', 'balance-'.$amount, false)
->update('bank_account');
// put the money in the wallet
$this->db->where('user_id', $user_id)
->set('balance', 'balance+'.$amount, false)
->update('wallet');
// release the locks
$this->db->query('unlock tables');
return true;
}
这使得任何其他MySQL连接对所述表的写操作都挂起,直到锁被释放。