循环直到密码是唯一的

时间:2011-09-28 07:15:04

标签: php mysql

这是一个文件共享网站。为了确保每个文件唯一的“密码”真正独一无二,我正在尝试这样做:

$genpasscode = mysql_real_escape_string(sha1($row['name'].time())); //Make passcode out of time + filename.
    $i = 0;
    while ($i < 1) //Create new passcode in loop until $i = 1;
    {
        $query = "SELECT * FROM files WHERE passcode='".$genpasscode."'";
        $res = mysql_query($query);
        if (mysql_num_rows($res) == 0) // Passcode doesn't exist yet? Stop making a new one!
        {
            $i = 1;
        }
        else // Passcode exists? Make a new one!
        {
            $genpasscode = mysql_real_escape_string(sha1($row['name'].time()));
        }
    }

如果两个用户同时在完全上传一个具有相同名称的文件,这实际上只会阻止双密码,但是他比安全感更好吗?我的问题是;这是按照我打算的方式工作吗?我无法可靠地(阅读:轻松)测试它,因为即使一秒钟关闭也会生成一个唯一的密码。

更新: 李建议我这样做:

do {
    $query = "INSERT IGNORE INTO files 
       (filename, passcode) values ('whatever', SHA1(NOW()))";
    $res = mysql_query($query);
} while( $res && (0 == mysql_affected_rows()) )

[编辑:我在上面的示例中进行了更新,以包含两个关键修复程序。请参阅下面的答案了解详情。 - @李]

但我担心它会更新别人的行。如果文件名和密码是数据库中唯一的字段,那么这不会有问题。但除此之外还有mime类型的检查等,所以我在想这个:

//Add file
        $sql = "INSERT INTO files (name) VALUES ('".$str."')";
        mysql_query($sql) or die(mysql_error());

        //Add passcode to last inserted file
        $lastid = mysql_insert_id();
        $genpasscode = mysql_real_escape_string(sha1($str.$lastid.time())); //Make passcode out of time + id + filename.
        $sql = "UPDATE files SET passcode='".$genpasscode."' WHERE id=$lastid";
        mysql_query($sql) or die(mysql_error());

那会是最好的解决方案吗? last-inserted-id字段始终是唯一的,因此密码也应该是唯一的。有什么想法吗?

UPDATE2:如果已存在,则IGNORE不会替换行。这对我来说是一种误解,所以这可能是最好的方式!

4 个答案:

答案 0 :(得分:6)

严格地说,您对唯一性的测试不能保证在并发负载下的唯一性。问题是您在插入行的位置之前(并且与其分开)检查唯一性以“声明”新生成的密码。另一个过程可能同时做同样的事情。这是怎么回事......

两个进程生成完全相同的密码。他们每个都从检查唯一性开始。由于两个进程都没有(还)向表中插入一行,因此两个进程都不会在数据库中找到匹配的密码,因此两个进程都会假定代码是唯一的。现在,随着每个流程继续工作,最终他们将两者使用生成的代码向files表插入一行 - 从而获得重复。

要解决此问题,您必须执行检查,并在单个“原子”操作中执行插入操作。以下是对这种方法的解释:


如果您希望密码是唯一的,则应将数据库中的列定义为UNIQUE。这将通过拒绝插入会导致重复密码的行来确保唯一性(即使您的PHP代码没有)。

CREATE TABLE files (
  id int(10) unsigned NOT NULL auto_increment PRIMARY KEY,
  filename varchar(255) NOT NULL,
  passcode varchar(64) NOT NULL UNIQUE,
)

现在,使用mysql的SHA1()NOW()生成密码作为插入语句的部分。将其与INSERT IGNORE ...docs)结合使用,并循环直到成功插入行:

do {
    $query = "INSERT IGNORE INTO files 
       (filename, passcode) values ('whatever', SHA1(NOW()))";
    $res = mysql_query($query);
} while( $res && (0 == mysql_affected_rows()) )

if( !$res ) {
   // an error occurred (eg. lost connection, insufficient permissions on table, etc)
   // no passcode was generated.  handle the error, and either abort or retry.
} else {
   // success, unique code was generated and inserted into db.
   // you can now do a select to retrieve the generated code (described below)
   // or you can proceed with the rest of your program logic.
}

注意:上面的示例已经过编辑,以说明@martinstoeckli在评论部分发布的优秀观察结果。进行了以下更改:

  • mysql_num_rows()docs)更改为mysql_affected_rows()docs) - num_rows不适用于插入。同时删除了mysql_affected_rows()的参数,因为此函数在连接级别上运行,而不是结果级别(在任何情况下,插入的结果都是布尔值,而不是资源编号)。
  • 在循环条件中添加了错误检查,并在循环退出后添加了错误/成功测试。错误处理很重要,因为没有它,数据库错误(如丢失的连接或权限问题)将导致循环永远旋转。上面显示的方法(使用IGNOREmysql_affected_rows(),并分别针对错误测试$res)允许我们将这些“真实数据库错误”与唯一约束违规区分开来(这完全是本节逻辑中的有效非错误条件)。

如果您需要在生成密码后获取密码,只需再次选择记录:

$res = mysql_query("SELECT * FROM files WHERE id=LAST_INSERT_ID()");
$row = mysql_fetch_assoc($res);
$passcode = $row['passcode'];

编辑:在上面的示例中更改了使用mysql函数LAST_INSERT_ID(),而不是PHP的函数。这是一种更有效的方法来完成同样的事情,结果代码更清晰,更清晰,更简洁。

答案 1 :(得分:3)

我个人会以不同的方式编写它,但我会为您提供更简单的解决方案:会话。

我猜你熟悉会话?会话是服务器端记住的变量,在某些时候超时,具体取决于服务器配置(默认值为10分钟或更长)。会话使用会话ID(随机生成的字符串)链接到客户端。

如果您在上传页面开始会话,则会生成一个ID,只要会话未被销毁,该ID就会保证是唯一的,这应该需要大约10分钟。这意味着,当您将会话ID和当前时间组合在一起时,您将永远不会拥有相同的密码。会话ID +当前时间(以微秒,毫秒或秒为单位)永远不会相同。

在您的上传页面中:

session_start();

在您处理上传的页面中:

$genpasscode = mysql_real_escape_string(sha1($row['name'].time().session_id()));
// No need for the slow, whacky while loop, insert immediately
// Optionally you can destroy the session id

如果你确实破坏了会话ID,那就意味着另一个客户端可以生成相同的会话ID的可能性非常小,所以我不建议这样做。我只是允许会话过期。

答案 2 :(得分:2)

你的问题是:

  

这是否按我打算的方式工作?

好吧,我会说......是的,确实有效,但可以进行优化。

<强>数据库

要确保数据库图层上的字段passcode中没有相同的值,请为此添加唯一键:

/* SQL */
ALTER TABLE `yourtable` ADD UNIQUE `passcode` (`passcode`);

(必须照顾重复的键处理而不是ofc)

<强>代码

等待一秒钟直到创建一个新的Hash,没关系,但如果你说的是重负荷,那么一秒钟可能是一个小小的永恒。因此,我宁愿将另一个组件添加到sha1 - 代码的一部分,也许是来自同一数据库记录的文件ID,用户ID或任何使其真正独特的内容。

如果你手边没有唯一的ID,你仍然可以回到随机数rand - php中的函数。

我不认为在这种情况下需要mysql_real_escape_stringsha1无论如何都返回一个40个字符的十六进制数字,即使你的行中有一些不好的字符。

$genpasscode = sha1(rand().$row['name'].time());

......应该足够了。

<强>风格

代码示例中使用了两次密码生成代码。将此移动到一个功能中开始清理它。

$genpasscode = gen_pc(row['name']);

...

function gen_pc($x) 
{
  return sha1($row[rand().$x.time());
}

如果我这样做,我会采用不同的方式,我会使用session_id()尽可能避免重复。这样您就不需要在该循环中循环并与数据库进行多次循环。

答案 3 :(得分:1)

您可以为表添加唯一约束。

ALTER TABLE files ADD UNIQUE (passcode);

PS:您可以使用microtimeuniqid来使密码更加独特。

修改 您尽可能在php中生成唯一值,并使用唯一约束来保证在数据库端。如果您的独特价值非常独特,但在极少数情况下它不是唯一的,请随时给出The system is busy now. Please try again:)这样的信息。