【问题标题】:Stop data inserting into a database twice停止向数据库中插入数据两次
【发布时间】:2010-10-12 21:58:47
【问题描述】:

我想知道当用户在与表单相同的页面上刷新时,其他程序员使用什么方法/预防措施来阻止数据被两次输入 MySQL 数据库?显然它发生了,我需要一个好的方法来阻止它。

【问题讨论】:

    标签: php user-input


    【解决方案1】:

    我称之为网络编程的黄金法则:

    永远不要用正文响应 POST 请求。总是做这项工作,然后用 Location: 标头响应以重定向到更新的页面,以便浏览器使用 GET 请求它。

    这样,刷新不会对你造成任何伤害。

    另外,关于 cmets 中的讨论。为了防止重复发布,例如意外双击提交按钮,将表单的 md5() 存储在文本文件中,并将新表单的 md5 与存储的 md5 进行比较。如果他们是平等的,你有一个双重职位。

    【讨论】:

    • 您好,莉雅,如果用户在浏览器中点击后退按钮然后再次前进会发生什么情况,我知道人们在同一个网站上经常有向后和前进的讨厌习惯!我无法承受两次相同的数据,谢谢 :)
    • Ben,我的做法是:收到新表单时,将其主要字段的 md5() 存储在一些 lastFormMD5.txt 中。现在,在处理表单时,比较新的 md5 和 lastFormMD5.txt 内容?平等的?比它是一个转发,忽略它:-)
    • jeroen,好的浏览器不会警告你 (Opera)。因为我们开发人员必须处理它,所以用户不在乎。他说F5,一定要刷新。用户会在没有阅读问题的情况下按“是”——用户是对的!
    • Ilya:每当我看到“你想再次 POST 到 example.com/chargeMyCreditCardAgain 吗?”时,我肯定会单击“否”。如果他们不能可靠地使用 POST,我肯定不想冒着他们搞砸其他事情的风险,太!后退按钮不应该引起副作用。
    • 对于重新提交存在问题的表单,我使用 nonces(“使用一次的数字”)。创建一个随机数/字符串,将其存储在会话数据中,并将其作为隐藏字段添加到表单中。提交表单时,检查 POSTed 值是否与会话值匹配,然后立即清除会话值。重新提交将失败。
    【解决方案2】:

    处理表单,然后重定向到结果页面。重新加载然后只重新显示结果页面。

    【讨论】:

      【解决方案3】:

      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
      

      表单提交一次后,不能再次提交,除非用户故意填写第二次。

      【讨论】:

        【解决方案4】:

        声明很明显(我还没有在这里看到...):永远不要使用 GET 来发布数据,始终使用 POST,这样如果用户尝试刷新 /re- 至少会收到警告发布页面(至少在 Firefox 中,但我想在其他浏览器中也是如此)。

        顺便说一句,如果您无法承受两次相同的数据,您还应该考虑使用唯一键(可以是字段的组合)的 MySQL 解决方案,并且:

            INSERT INTO ... ON DUPLICATE KEY UPDATE ...
        

        【讨论】:

          【解决方案5】:

          我同意 Ilya 的观点,并补充说,一旦单击“提交”按钮,您应该使用一些客户端 JavaScript 来禁用它,或者显示一个模式对话框(css 可以在此处为您提供帮助)以避免多次单击提交按钮.

          最后,如果您不希望数据库中的数据两次,那么在尝试插入数据之前还要检查数据库中的数据。如果您确实允许重复记录但不希望从单一来源快速重复插入,那么我会使用时间/日期戳和 IP 地址字段来允许在我的提交代码中进行基于时间的“锁定”,即如果IP 相同且上次提交时间小于 5 分钟前不要插入新记录。

          希望能给你一些想法。

          【讨论】:

          • 禁用提交按钮有一个缺点:猜猜如果用户有一个临时的互联网中断会发生什么?就像,在填写表格时。现在他被一张填好的表格困住了,他无法提交。所以我认为最好使用其他类型的保护:-)
          • 在找到 Prototype Javascript 库之前,我一直认为是这种情况。他们的 AJAX 命令将触发自定义返回函数,即使网络中断(因此您可以正确显示和发布失败的表单)。
          【解决方案6】:

          您可能想查看大多数现代 Web 应用程序实现的 POST/Redirect/GET 模式,请参阅http://en.wikipedia.org/wiki/Post/Redirect/Get

          【讨论】:

            【解决方案7】:

            除了已经提到的关于让用户离开发布页面以便刷新和返回按钮无害的好建议之外,改进数据存储的另一层是使用UUIDs 作为表中的键并让您的应用程序会生成它们。

            这些在 Microsoft 世界中也称为 GUID,在 PHP 中,您可以通过 PHP 中的 uniqid() 生成一个。这是一个 32 个字符的十六进制值,您应该以十六进制/二进制列格式存储它,但如果该表不会被大量使用,那么 CHAR(32) 将起作用。

            当您将表单显示为隐藏输入时生成此 ID,并确保将数据库列标记为主键。现在,如果用户确实设法返回到发布页面,则 INSERT 将失败,因为您不能有重复的键。

            另外一个好处是,如果您在代码中生成 UUID,那么在执行插入之后,您将永远不需要使用浪费的查询来检索生成的密钥,因为您已经知道它了。当您需要将子项插入其他表时,这是一个很好的好处。

            良好的编程基于您的工作分层,而不是依赖于一件事情来工作。尽管编码人员依赖增量 ID 很常见,但它们是构建表的最懒惰的方式之一。

            【讨论】:

              【解决方案8】:

              我通常依赖于 sql UNIQUE 索引约束。 http://dev.mysql.com/doc/refman/5.0/en/constraint-primary-key.html

              【讨论】:

                【解决方案9】:

                您必须在 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

                【讨论】:

                  【解决方案10】:

                  嗯,首先,为了最小化,你应该这样做,所以他们必须做表单发布来插入数据。这样,至少他们会得到一个很好的确认对话框,询问他们是否真的想重新提交。

                  更复杂的是,您可以在每个表单中放置一个隐藏的一次性使用密钥,一旦提交了具有该密钥的表单,当他们尝试提交具有相同密钥的表单时会显示错误。要创建此密钥,您可能需要使用 GUID 之类的东西。

                  【讨论】:

                    【解决方案11】:

                    添加一个带有随机字符串的隐藏字段(例如由md5(uniqid()) 生成),在数据库中为该字符串创建一个字段并将其设为唯一。

                    【讨论】:

                      【解决方案12】:

                      您可以使用令牌来防止页面再次被处理! 很多网络框架都使用了这样的程序!

                      您应该使用的模式是“同步器令牌模式”! 如果您有面向服务的应用程序,您可以将您的状态保存在数据库中。

                      数据可以通过 JavaScript 或隐藏的表单域发送。

                      您还应该看看那些开箱即用支持此类功能的库! Grails就是这样一个!

                      见:http://www.grails.org/1.1-Beta3+Release+Notes ...

                      <g:form useToken="true">
                      

                      ...

                      withForm {
                         // good request
                      }.invalidToken {
                         // bad request
                      }
                      

                      ..

                      【讨论】:

                        【解决方案13】:

                        我的两分钱:

                        • 发送数据后将用户重定向到同一页面并验证if(isset($_POST['submit'])

                        类似案例的其他有用信息:

                        【讨论】:

                        • Table LOCKing 通常是一个可怕的想法,在这种情况下它对他毫无帮助。
                        • @TravisO:我不同意LOCK/UNLOCK的“可怕想法”,但我同意你在逗号后面写的,所以我编辑了我的答案
                        【解决方案14】:

                        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?)。

                        【讨论】:

                          【解决方案15】:

                          尝试在表单中包含一些内容以防止重复提交,最好同时防止跨站点请求伪造。我建议使用我称之为 formkey 的东西,这是一个一次性字段,用于唯一标识表单提交,将其绑定到位于个人地址的个人用户。这个概念也有其他名称,但我链接到的简短说明已经很好地解释了它。

                          【讨论】:

                            【解决方案16】:

                            避免在页面刷新时重复插入记录的最佳方法是, 单击按钮在数据库中插入记录后,只需添加以下行:

                            Response.Write("<script>location.href='yourpage.aspx'</script>");
                            

                            【讨论】:

                              猜你喜欢
                              • 1970-01-01
                              • 1970-01-01
                              • 1970-01-01
                              • 1970-01-01
                              • 1970-01-01
                              • 1970-01-01
                              • 1970-01-01
                              • 2021-05-26
                              • 1970-01-01
                              相关资源
                              最近更新 更多