Петля до тех пор, пока код доступа не станет уникальным

Это для сайта обмена файлами. Чтобы убедиться, что «код доступа», который уникален для каждого файла, действительно уникален, я пытаюсь это сделать:

$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()) ) 

[ Изменить : я обновил выше пример, чтобы включить в себя два важных исправления. Подробнее см. Мой ответ ниже. – @ Lee]

Но я боюсь, что он обновит чужую строку. Что не было бы проблемой, если имя файла и пароль были единственными полями в базе данных. Но в дополнение к этому есть также проверки типа 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-insert-id всегда уникально, поэтому пароль должен быть слишком. Есть предположения?

UPDATE2: Apperenatly IGNORE не заменяет строку, если она уже существует. Это было непонимание с моей стороны, так что, наверное, это лучший способ!

Строго говоря, ваш тест на уникальность не гарантирует уникальность при одновременной нагрузке. Проблема в том, что вы проверяете уникальность до (и отдельно от) места, где вы вставляете строку, чтобы «требовать» ваш новый сгенерированный код доступа. В то же время другой процесс может быть одним и тем же. Вот как это происходит …

Два процесса генерируют тот же самый пароль. Каждый из них начинает проверку на уникальность. Так как ни один из процессов не ввел строку в таблицу, оба процесса не найдут подходящего кода доступа в базе данных, и поэтому оба процесса будут считать, что код уникален. Теперь, когда процессы продолжают работу, в конце концов они оба вставляют строку в таблицу 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, ) 

Теперь используйте SHA1() и NOW() для mysql для генерации вашего кода доступа как часть инструкции insert. Объедините это с 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. Это более эффективный способ выполнить одно и то же, и полученный код чище, яснее и менее загроможден.

Я бы лично написал его по-другому, но я дам вам гораздо более простое решение: сеансы.

Думаю, вы знакомы с сеансами? Сессии – это запоминаемые переменные на стороне сервера, которые тайм-аут в какой-то момент, в зависимости от конфигурации сервера (значение по умолчанию – 10 минут или дольше). Сессия связана с клиентом с использованием идентификатора сеанса, случайной сгенерированной строки.

Если вы запустите сеанс на странице загрузки, будет создан идентификатор, который гарантированно будет уникальным, поскольку сеанс не будет уничтожен, что займет около 10 минут. Это означает, что когда вы комбинируете идентификатор сеанса и текущее время, у вас никогда не будет одинакового кода доступа. Идентификатор сеанса + текущее время (в микросекундах, миллисекундах или секундах) НИКОГДА не меняется.

На странице загрузки:

 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 

Если вы уничтожите идентификатор сеанса, это будет означать, что существует очень небольшой шанс, что другой клиент может сгенерировать один и тот же идентификатор сеанса, чтобы я не советовал это. Я просто разрешу сеансу истекать.

Ваш вопрос:

это работает так, как я это намерен?

Ну, я бы сказал … да, это действительно работает, но это может быть оптимизировано.

База данных

Чтобы не иметь то же значение в полевом passcode на уровне базы данных, добавьте к нему уникальный ключ:

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

(необходимо обработать дублирующее ключевое управление, не считая)

Код

Чтобы подождать секунду, пока не будет создано новое Hash, все в порядке, но если вы говорите о большой нагрузке, то одна секунда может быть крошечной вечностью. Поэтому я предпочел бы добавить еще один компонент в sha1 часть вашего кода, возможно, идентификатор файла из той же записи базы данных, userid или что-то другое, что делает это действительно уникальным.

Если у вас нет уникального идентификатора, вы все равно можете вернуться к случайному номеру 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() чтобы избежать дубликатов как можно лучше. Таким образом, вам не понадобится цикл и связь с вашей базой данных в этом цикле, возможно, несколько раз.

Вы можете добавить уникальное ограничение для своей таблицы.

 ALTER TABLE files ADD UNIQUE (passcode); 

PS: Вы можете использовать microtime или uniqid чтобы сделать код доступа более уникальным.

Изменить: вы делаете все возможное, чтобы генерировать уникальное значение в php, а уникальное ограничение используется для гарантии того, что на стороне базы данных. Если ваше уникальное значение очень уникально, но в очень редком случае оно не может быть уникальным, просто не стесняйтесь давать такое сообщение, как The system is busy now. Please try again:) The system is busy now. Please try again:) .