【发布时间】:2010-10-12 21:58:47
【问题描述】:
我想知道当用户在与表单相同的页面上刷新时,其他程序员使用什么方法/预防措施来阻止数据被两次输入 MySQL 数据库?显然它发生了,我需要一个好的方法来阻止它。
【问题讨论】:
标签: php user-input
我想知道当用户在与表单相同的页面上刷新时,其他程序员使用什么方法/预防措施来阻止数据被两次输入 MySQL 数据库?显然它发生了,我需要一个好的方法来阻止它。
【问题讨论】:
标签: php user-input
我称之为网络编程的黄金法则:
永远不要用正文响应 POST 请求。总是做这项工作,然后用 Location: 标头响应以重定向到更新的页面,以便浏览器使用 GET 请求它。
这样,刷新不会对你造成任何伤害。
另外,关于 cmets 中的讨论。为了防止重复发布,例如意外双击提交按钮,将表单的 md5() 存储在文本文件中,并将新表单的 md5 与存储的 md5 进行比较。如果他们是平等的,你有一个双重职位。
【讨论】:
处理表单,然后重定向到结果页面。重新加载然后只重新显示结果页面。
【讨论】:
Ilya 的回答是正确的,我只是想补充一点,而不是评论:
如果重新提交是危险的(返回并再次提交,重新加载结果页面[如果你没有接受 Ilya 的建议],等等)我使用“nonce”来确保表单只能通过一次。
在表单页面上:
<?php
@session_start(); // make sure there is a session
// store some random string/number
$_SESSION['nonce'] = $nonce = md5('salt'.microtime());
?>
// ... snip ...
<form ... >
<input type="hidden" name="nonce" value="<?php echo $nonce; ?>" />
</form>
在处理页面中:
<?php
if (!empty($_POST)) {
@session_start();
// check the nonce
if ($_SESSION['nonce'] != $_POST['nonce']) {
// some error condition
} else {
// clear the session nonce
$_SESSION['nonce'] = null;
}
// continue processing
表单提交一次后,不能再次提交,除非用户故意填写第二次。
【讨论】:
声明很明显(我还没有在这里看到...):永远不要使用 GET 来发布数据,始终使用 POST,这样如果用户尝试刷新 /re- 至少会收到警告发布页面(至少在 Firefox 中,但我想在其他浏览器中也是如此)。
顺便说一句,如果您无法承受两次相同的数据,您还应该考虑使用唯一键(可以是字段的组合)的 MySQL 解决方案,并且:
INSERT INTO ... ON DUPLICATE KEY UPDATE ...
【讨论】:
我同意 Ilya 的观点,并补充说,一旦单击“提交”按钮,您应该使用一些客户端 JavaScript 来禁用它,或者显示一个模式对话框(css 可以在此处为您提供帮助)以避免多次单击提交按钮.
最后,如果您不希望数据库中的数据两次,那么在尝试插入数据之前还要检查数据库中的数据。如果您确实允许重复记录但不希望从单一来源快速重复插入,那么我会使用时间/日期戳和 IP 地址字段来允许在我的提交代码中进行基于时间的“锁定”,即如果IP 相同且上次提交时间小于 5 分钟前不要插入新记录。
希望能给你一些想法。
【讨论】:
您可能想查看大多数现代 Web 应用程序实现的 POST/Redirect/GET 模式,请参阅http://en.wikipedia.org/wiki/Post/Redirect/Get
【讨论】:
除了已经提到的关于让用户离开发布页面以便刷新和返回按钮无害的好建议之外,改进数据存储的另一层是使用UUIDs 作为表中的键并让您的应用程序会生成它们。
这些在 Microsoft 世界中也称为 GUID,在 PHP 中,您可以通过 PHP 中的 uniqid() 生成一个。这是一个 32 个字符的十六进制值,您应该以十六进制/二进制列格式存储它,但如果该表不会被大量使用,那么 CHAR(32) 将起作用。
当您将表单显示为隐藏输入时生成此 ID,并确保将数据库列标记为主键。现在,如果用户确实设法返回到发布页面,则 INSERT 将失败,因为您不能有重复的键。
另外一个好处是,如果您在代码中生成 UUID,那么在执行插入之后,您将永远不需要使用浪费的查询来检索生成的密钥,因为您已经知道它了。当您需要将子项插入其他表时,这是一个很好的好处。
良好的编程基于您的工作分层,而不是依赖于一件事情来工作。尽管编码人员依赖增量 ID 很常见,但它们是构建表的最懒惰的方式之一。
【讨论】:
我通常依赖于 sql UNIQUE 索引约束。 http://dev.mysql.com/doc/refman/5.0/en/constraint-primary-key.html
【讨论】:
您必须在 showAddProductForm() 方法中将一个 uniqid 变量传递到您的 html 中,并将相同的 uniqid 传递到您的 $_SESSION 例如:
public function showAddProductForm()
{
$uniId = uniqid();
$_SESSION['token'][$uniId] = '1';
$fields['token'] = 'token['.$uniId.']';
$this->fileName = 'product.add.form.php';
$this->template($fields);
die();
}
然后,您必须在表单中的 HTML 代码中添加一个隐藏输入,其中包含从 showAddProductForm 方法传递到 HTML 中的 uniqid 值。
<input type="hidden" name="<?=$list['token']?>" value="1">
在提交事件之后,您将在 addProduct() 方法的开头对其进行分析。如果令牌存在于 $_SESSION 中并且它在该数组中具有相等的值,那么这就是新的请求。将他重定向到正确的页面并取消设置令牌并继续插入。否则是来自重新加载页面或重复请求,将他重定向到 addProdeuct 页面
public function addProducts($fields)
{
$token_list = array_keys($fields['token']);
$token = $token_list['0'];
if (isset($_SESSION['token'][$token]) and $_SESSION['token'][$token] == '1') {
unset($_SESSION['token'][$token]);
} else {
$this->addAnnounceForm($fields, '');
}
}
也许你会问为什么一个标记数组为什么不是一个变量。因为在管理面板中,用户打开多个选项卡并插入多个选项卡,所以如果他们使用多个选项卡,此算法将失败。
特别感谢发现这个方法的人malekloo
【讨论】:
嗯,首先,为了最小化,你应该这样做,所以他们必须做表单发布来插入数据。这样,至少他们会得到一个很好的确认对话框,询问他们是否真的想重新提交。
更复杂的是,您可以在每个表单中放置一个隐藏的一次性使用密钥,一旦提交了具有该密钥的表单,当他们尝试提交具有相同密钥的表单时会显示错误。要创建此密钥,您可能需要使用 GUID 之类的东西。
【讨论】:
添加一个带有随机字符串的隐藏字段(例如由md5(uniqid()) 生成),在数据库中为该字符串创建一个字段并将其设为唯一。
【讨论】:
您可以使用令牌来防止页面再次被处理! 很多网络框架都使用了这样的程序!
您应该使用的模式是“同步器令牌模式”! 如果您有面向服务的应用程序,您可以将您的状态保存在数据库中。
数据可以通过 JavaScript 或隐藏的表单域发送。
您还应该看看那些开箱即用支持此类功能的库! Grails就是这样一个!
见:http://www.grails.org/1.1-Beta3+Release+Notes ...
<g:form useToken="true">
...
withForm {
// good request
}.invalidToken {
// bad request
}
..
【讨论】:
我的两分钱:
if(isset($_POST['submit'])
类似案例的其他有用信息:
在第一次点击后通过 JavaScript 禁用按钮
【讨论】:
POE (Post Once Exactly) 是一种 HTTP 模式,旨在警告客户端使用专有标头阻止双重提交...
GET /posts/new HTTP/1.1
POE: 1
...
...但仍在规范中。
http://www.mnot.net/drafts/draft-nottingham-http-poe-00.txt
我认为上述随机数是一个很好的解决方案。尽管将 nonce 存储为离散会话变量会在客户端尝试从多个选项卡执行同时发布时引入一些错误。也许更好...
$_SESSION['nonces'][] = $nonce;
...和...
if (in_array($_POST['nonce'], $_SESSION['nonces'])) {
...允许多个随机数(nonci?noncei?)。
【讨论】:
尝试在表单中包含一些内容以防止重复提交,最好同时防止跨站点请求伪造。我建议使用我称之为 formkey 的东西,这是一个一次性字段,用于唯一标识表单提交,将其绑定到位于个人地址的个人用户。这个概念也有其他名称,但我链接到的简短说明已经很好地解释了它。
【讨论】:
避免在页面刷新时重复插入记录的最佳方法是, 单击按钮在数据库中插入记录后,只需添加以下行:
Response.Write("<script>location.href='yourpage.aspx'</script>");
【讨论】: