这是一个文件共享网站。为了确保每个文件唯一的“密码”真正独一无二,我正在尝试这样做:
$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不会替换行。这对我来说是一种误解,所以这可能是最好的方式!
答案 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()
的参数,因为此函数在连接级别上运行,而不是结果级别(在任何情况下,插入的结果都是布尔值,而不是资源编号)。IGNORE
和mysql_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_string。 sha1
无论如何都返回一个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:您可以使用microtime
或uniqid
来使密码更加独特。
修改强>
您尽可能在php中生成唯一值,并使用唯一约束来保证在数据库端。如果您的独特价值非常独特,但在极少数情况下它不是唯一的,请随时给出The system is busy now. Please try again:)
这样的信息。