Как правильно предотвратить повторную отправку формы?
Например, написание сообщения пользователем: после заполнения формы и нажатия кнопки Отправить, появляется страница, где написано сообщение Отправлено, а ниже список всех сообщений. Если обновить страницу в Опере, то сообщение еще раз отправится, если в Хроме, то спросит Отправить ли форму еще раз, а нужно обновить страницу с сообщениями не спрашивая не о чем.
- Если делать со скрытым полем со случайным значением, и проверять это значение, то повторная отправка всё равно будет спрашиваться, хоть и не добавится уже.
- Если после добавления сообщения в базу делать перенаправление на страницу вида index.php?message=Сообщение%20успешно%20добавлено! , то это сообщение после обновления страницы так и будет появляться.
+ Поэтому я хочу сделать так: на сайте используются свои сессии в базе MySQL, но чтобы не напрягать базу, я хочу использовать встроенные в php сессии как раз для этого дела, а именно: в месте вывода сообщения об успешной отправке, то есть после физического добавления информации в базу, стартовать сессию, в нее добавить переменную с сообщением "Сообщение успешно добавлено!", затем сделать перенаправление на эту же страницу, где выводятся все сообщения. А в коде самой страницы, стартовать сессию, и если есть в ней переменная с сообщением, то вывести его и удалить переменную.
+ А еще лучше, сделать массив сообщений и выводить их все, но если вдруг открыто несколько вкладок, то есть очень малая вероятность, что будет что-то одновременно и отобразится сообщение не в той вкладке, поэтому наверно стоит сделать какое-то случайное имя переменной, и передавать это имя GET параметром в url перенаправления, и уже по нему выводить и удалять переменную. Тогда вроде всё нормально.
Как Вам такой вариант, и как вообще это правильно делать, может, всё-таки отдельную страницу, как на многих форумах "Сообщение добавлено, сейчас Вы будете перенаправлены"?
Я бы не стал хранить массив сообщений в сессии, это вообще дурная практика что-либо складировать в неё, кроме пары-тройки идентификаторов.
Для гарантии от дублирования есть приём:
- на сервере генерируется идентификатор нового сообщения
- сервер передает этот идентификатор клиенту
- клиент заполняет сообщение и отсылает его серверу с тем же самым идентификатором
- сервер при получении ставит в соответствие идентификатору сообщение (т.е. заполняет соответствующее поле в БД, если оно было пустым)
Рассмотрим теперь сценарии при попытке отправки с клиента сообщения:
- идентификатор сообщения задан:
-- на сервере уже есть сообщение с таким id => отказ
-- на сервере нет такого id => отказ (возможно попытка поиска уязвимости)
- идентификатор не задан => отказ
Сообщение в сессии будет храниться менее 1 секунды (время редиректа с одной страницы на другую), затем сразу вывод и удаление сообщения из сессии. В базу, я думаю, еще хуже, поэтому сессия оптимальный вариант.
Сессия для этого и сделана, чтобы хранить различные параметры, а не идентификаторы, имя сессии и есть идентификатор для складирования параметров, конечно лучше числовых и небольших, но зачем усложнять, делать коды сообщений, и по коду выводить соответствующее, если можно сразу 100 байт сообщения сохранить в сессию и тут же удалить.
А на счет дублирования, можно также использовать сессию, создать случайное значение, вывести его в скрытое поле формы, затем после отправки формы, если есть значение, то действие выполнить и значение удалить, если значения нет, то ничего не делать.
@Sekira а как вы будете решать вопрос с горизонтальным масштабированием, когда придется использовать другой обработчик сессии, например на memcached и локами на запись?
Сравнил сейчас скорости, применительно для своего скрипта, примерно среднее время за ~30 попыток:
Сессии: создание сессии и добавление сообщения 0,0002 + создание сессии и вывод сообщения 0,0002
MySQL: редактирование поля в сессии пользователя 0,0004 + вывод сообщения (строка из таблицы итак считывается и не учитывается) 0,000018
Хотя, если запускать сессию придется постоянно для проверки, есть сообщения или нет, то с базой предпочтительнее, так как только один раз будет задержка при редактировании поля, а при выводе уже не будет, а там постоянно, даже если никаких сообщений не добавлять, хотя можно конечно сделать при редиректе пометку в get параметре, что было добавлено сообщение и только в эти моменты делать старт сессии, но наверное с базой по-лучше будет.
А забыл после вывода сообщения очистить поле/параметр, тогда к сессиям еще +0,0002, а к БД +0,0004, тогда сессии лучше, только с параметром, чтобы не каждый раз проверять есть ли сообщения, а только когда только что добавили и кто-нибудь обновил страницу эту с параметром.
Тогда может вообще cookie использовать =) Добавлять после добавления сообщения, выводить и удалять после редиректа.
@Sekira да, как вариант сгодится. А так вообще описанный мной способ применяется более широкого спектра задач, например для работы с платежами по API и таким образом можно избежать задвоения транзакций.
@Sekira, куки не помогут защититься от повторной отправки формы при дабл-клике на кнопке "отправить". @akubintsev дело предложил, но вы о разном говорите. Я вас помирю:
- хранить в сессии нужно "идентификатор нового сообщения" (сессия для этого годится, не вижу проблемы при горизонтальном масштабировании и мемкэше). Тогда чей первый запрос придет, тот и удалит это значение из сессии, таким образом второй запрос не пройдет сверку.
- если же этот идентификатор хранить в куке, то оба запроса пришлют его на сервер вместе с формой (при дабл клике) и я не знаю как без еще одного места (бд или сессии) проверить кто из них дубликат.
- само сообщение об успешном добавлении тоже можно пихнуть в сессию, в фреймворках такое используется (обычно известно как flash message). Но можно и в куку его писать (если выводить с htmlspecialchars, правильно мыслишь :)
Если делать следующим способом, то куки очень даже подойдут:
1) Форма без всяких случайных значений и т.п., обычная форма, пользователь заполняет и нажимает кнопку;
2) Всё проверяется, если ошибки, то выводятся и форма остается, если ошибок нет, запись добавляется в базу, ставится кука с сообщением, редирект на страницу со списком сообщений, exit;
3) Список сообщений, если есть переменная в куках с сообщением, то выводим и удаляем, если нет, то ничего не делаем.
Теперь если обновлять страницу то ничего еще раз не добавится, если вернуться назад и еще раз нажать кнопку, то конечно добавится, но от этого я и не хочу защититься.
А если добавить еще один сервер-обработчик (читай, вэб-сервер)? Сессии отпадают. Хранить в куках - не забывайте про то, что они отправляются при каждом запросе к серверу... По моему, вариант с БД предпочтительнее.
Куки создаются и тут же после редиректа и вывода сообщения, удаляются, поэтому всё нормально. Может не очень красиво, зато быстро работает и url чистое.
Да, вы вполне корректно описали решение проблемы с повторной отправкой формы.
Однако лучше остановится на чем-то одном.
Перенаправлять пользователя и хранить массив сообщений в сессии. Выводить сообщения, потом удалять. Беспокоится о том, что пользователь заполнит форму, откроет другую вкладку с вашим сайтом, потом перейдет обратно, где была форма и это за 50-200 мс, думаю не стоит. Так сделано в ZF1 и ZF2 и много наверное где.
Второй вариант - перенаправлять пользователя по другому адресу, тоже можно, но это не так элегантно как в первом варианте.
О, спасибо, не знал что в Zend Framework'е так сделано, получается, прямо как я описывал.
Да, может и не стоит беспокоиться, портить внешний вид url какими-то случайными данными, даже если кто-то одновременно нажмет в нескольких вкладках отправить, то с данными ничего страшного не случится, просто в одной или в обоих отобразится два сообщения. Ситуация почти невозможная, если делать вручную, это надо нажать в одной вкладке кнопку и сразу перейти на вторую и снова нажать, если только проблемы с интернетом у пользователя или бот какой-нибудь, то может случиться, но ничего критичного, я думаю.
Зачем Вы обнуляете $_POST, если между обнулением и exit(); ничего использовавшее этот $_POST нет? Я думаю, Вы можете удалить строку $_POST = NULL; и всё будет работать также.
Я тоже, но наверное надо заставлять себя в тех местах, где уж точно уверен, удалять проверки и т.п. Хотя сам бывает оставляю, на всякий случай, если думаю, что вдруг когда-нибудь будет редактироваться код и забуду, что там вот так вот сделано.
@Sekira А поддерживать Netscape Navigator 1.0 ? Сколько пользователей заходят на ваш сайт с выключенным js, сами того не осознавая? Те, кто намеренно его выключают, сами идут этот шаг и знают последствия. Что ж, возможно, когда-то на ваш сайт зайдут с микроволновки, но и стандарты W3C к тому времени могут поменяться - что же делать? Как думаете, если человек сегодня пользуется браузером без js, то сколько современных ресурсов ему будет доступно?
Конечно, усердствовать со скриптами тоже не нужно, но по факту - можете привести кейс, где критически важна поддержка типичного проекта (не сервера для 3д-телевизора), при чем, которая разрабатывается фрилансером или типичной студией\командой, а не корпорацией масштаба Яндекса или Гугла (да и то, контакт, фейсбук, ютуб, без него не работают).
А без кейса - это преждевременная оптимизация.
Я считаю, что отправку формы всё-таки можно сделать и стандартным способом, а вот, например, подгружаемые выпадающие списки, можно и на JS, так как без него будет уж совсем "некрасиво". А если уж делать многое на JS, то делать всё, то есть без перезагрузки страницы и т.п. Хотя иногда раздражает на подобных сайтах, что скрипт какой-нибудь недогрузился и на странице ничего не работает, пока не обновишь страницу, чтобы скрипты все загрузились, а кто сильно не связан с компьютерами, будет думать, что случилось, хорошо если перезапустит браузер и всё заработает.
@Sekira Нельзя быть "немножко беременной". Если Вы сделали выпадающие списки на JS и пользователь с отключенным JS ничего не смог выбрать и не заполнил форму, то какая ему разница, работает у него отправка этой формы или нет.
Выпадающий список работает и без JS, допустим список, Страна и в зависимости от страны загружаются области, если пользователь без JS, выбирает страну, то во втором поле ничего не загружается, но он может нажать кнопку Submit и будет ошибка "Вы не выбрали область", и вместе с этой ошибкой загрузится список =)
@Rsa97 и тут я заплакал)))
Поддерживаю. Уже давно на всех проектах отправка форм только аяксом. На фэйк-линках висят соответствующие действию ссылки, но все, как одна сообщают о необходимости включить JS.
Аналитика упрямо утверждает, что "отказов" по этим линкам нет.
Это лишь немного усложняет код (отображение списка в зависимости от первого поля, тот же самый код, как и в скрипте ответа для JS).
А вот JS проверка полей, это как раз усложняет код, пользователю то конечно удобнее, когда сразу все ошибки подсвечиваются, но это двойная проверка, проверять на JS, а потом эти же поля проверять перед использованием в скрипте, так как они могут содержать что угодно.
Короче все просто..
после обработки данных сохраняете массив в сессии и делаете редирект на другую функцию, в другой функции проверяете на наличие сессии и выводите массив. Вот и все теперь при переходе на другую страницу не будет запроса на перезагрузку)).