Подтверждение телефона кодом из SMS - отправка, срок жизни, попытки
Подтверждаем, что номер принадлежит посетителю: отправляем одноразовый код и принимаем его обратно с формы. Сценарий нужен при регистрации по телефону и при смене номера в профиле.
Решение
Код и его хранение
Генерируем код и кладём его в хранилище:
use Bitrix\Main\Data\Storage\PersistentStorageInterface;use Bitrix\Main\DI\ServiceLocator;
$storage = ServiceLocator::getInstance()->get(PersistentStorageInterface::class);$code = (string)random_int(100000, 999999); // шестизначный одноразовый код$key = 'shop.phone_confirm.' . $token; // ключ вида модуль.фича.идентификатор
$storage->set($key, ['hash' => hash('sha256', $code), 'left' => 3], 300);Хранилище ядра само удалит запись через пять минут: метод set требует явного
положительного срока жизни. Открытый код на сервере не держат, рядом с остатком
попыток лежит его хеш. Интерфейс хранилища доступен с версии главного модуля
25.1100.0, раньше брали свою таблицу.
Привязка к сеансу
Привязываем код к сеансу посетителя:
$session = \Bitrix\Main\Application::getInstance()->getSession();if (!$session->has('phone_confirm_token')) { $session->set('phone_confirm_token', \Bitrix\Main\Security\Random::getString(32));}$token = $session['phone_confirm_token']; // объект сессии поддерживает ArrayAccessКлюч хранилища собирается из случайного значения сеанса, а не из номера телефона. Иначе код, заказанный одним посетителем, подтвердит форму, открытую в чужом браузере.
Отправка сообщения
Отправляем код событием с шаблоном:
use Bitrix\Main\Sms\Event;
$result = (new Event('USER_CONFIRM_PHONE', ['CODE' => $code, 'USER_ID' => $userId])) ->setSite('s1') // шаблон ищется по событию, сайту и языку ->send(true); // true - сразу, минуя очередь
if (!$result->isSuccess()) { $logger = \Bitrix\Main\Diag\LoggerFactory::createById('sms'); // приёмник задаёт .settings.php if ($logger) { $logger->error('код не ушёл: {errors}', ['errors' => implode('; ', $result->getErrorMessages())]); }}Текст сообщения живёт в шаблоне события, где метка #CODE# заменяется значением.
Отправку здесь делают немедленной: посетитель ждёт код на экране, а очередь отдаёт
сообщение только после завершения запроса. Гостю, у которого учётной записи ещё
нет, номер передают прямым вызовом SmsManager::sendMessageDirectly.
Пауза перед повторным запросом
Ставим паузу между запросами кода:
$guard = 'shop.phone_confirm_guard.' . $phone; // ограничитель считает по номеруif ($storage->get($guard) !== null) { throw new \RuntimeException('код уже отправлен, повтор через минуту');}$storage->set($guard, 1, 60); // не чаще одной отправки в минутуОграничитель ведут по номеру, а не по сеансу. Чистка куков обнуляет сеанс за секунду, а номер посетителя остаётся прежним. Обратный отсчёт в браузере - только подсказка, а настоящую проверку делают на сервере.
Проверка ввода и счётчик попыток
Сверяем введённый код и считаем попытки:
$state = $storage->get($key);if ($state === null) { throw new \RuntimeException('срок действия кода истёк'); // запись уже удалена}if (!hash_equals($state['hash'], hash('sha256', $input))) { $state['left']--; $state['left'] > 0 ? $storage->set($key, $state, 300) : $storage->delete($key); throw new \RuntimeException('код неверный, осталось попыток: ' . $state['left']);}$storage->delete($key); // код одноразовый, гасим при успехеТри попытки закрывают опечатку и не дают перебрать шестизначное число. Повторная запись назначает срок жизни заново, поэтому окно растягивается и настоящим ограничителем работает счётчик. Запись после успеха удаляют сразу, иначе один код подтвердит и второй номер.
Проверка номера перед отправкой
Проверяем номер в общем обработчике:
\Bitrix\Main\EventManager::getInstance()->addEventHandler('main', 'onBeforeSendSms', function (\Bitrix\Main\Event $event) { $msg = $event->getParameter('message'); if (!preg_match('/^\+?[1-9]\d{7,14}$/', $msg->getTo())) { return new \Bitrix\Main\EventResult(\Bitrix\Main\EventResult::ERROR); } });Одна точка проверки исходящих сообщений выгоднее россыпи проверок по коду формы. Возврат результата с ошибкой отменяет отправку целиком, и деньги за сообщение на заведомо неверный номер не спишутся.
Типичные проблемы
Профиль создан, а код так и не введён.
Учётная запись заводится до подтверждения номера и остаётся неактивной до ввода кода. Повторная регистрация того же номера падает с сообщением, что такой пользователь уже существует.
Посетитель ошибся номером и не может начать заново.
Почта уже занята той неактивной записью, которую он сам создал минуту назад. Проверку занятости ведут с оглядкой на признак активности, а неподтверждённые записи чистят по расписанию.
Код приходит через минуту после нажатия кнопки.
Отправка ушла в общую очередь и ждёт завершения запроса и фонового задания. Коду подтверждения нужна немедленная отправка, иначе его срок жизни истечёт раньше доставки.
Код угадывают перебором с формы.
Остаток попыток не хранится, и шестизначное число подбирается скриптом за несколько минут. Счётчик попыток держат рядом с кодом и удаляют запись при их исчерпании.
Частые вопросы
Не приходит смс на номер при подтверждении регистрации.
Сначала проверяют результат отправки и настройки провайдера, а не код формы. Отправка возвращает объект результата, и без его проверки отказ провайдера проходит молча. Дальше смотрят внешний статус сообщения у оператора.
Сколько должен жить код подтверждения?
Обычно от трёх до пяти минут: этого хватает на доставку сообщения и ввод. Срок задают явно, потому что хранилище ядра не принимает запись без него и не разрешает больше семи суток.
Как дать выбор: код в смс или на почту?
Канал выбирают в обработчике события регистрации OnAfterUserRegister, читая значение переключателя из полей формы. Дальше вызывают отправку сообщения или почтовое событие, а хранение кода и счётчик попыток остаются общими.
Можно ли подтверждать номер при заказе от гостя?
Да, механизм тот же: код в хранилище, проверка перед сохранением заказа. Отличие в том, что учётной записи ещё нет, поэтому ключ привязывают к сеансу оформления, а не к идентификатору пользователя.
Где хранить код - в сессии или в базе?
В хранилище ядра со сроком жизни, а в сессии держат только случайный признак сеанса. Сессия обнуляется при смене устройства и при чистке куков, а запись хранилища переживает запрос и истекает сама.
Смежное
- SMS с сайта - оглавление подтемы
- SMS и очереди сообщений - канал сообщений
- СМС из кода: событие и шаблон, прямая отправка - механика отправки
- Свой провайдер SMS: класс, регистрация, статусы - оператора нет в поставке
- SMS не доходят до получателя: разбор причин - код есть, сообщения нет
- Регистрация покупателей: форма, подтверждение, свои поля - куда встраивается проверка
- Защита входа: перебор паролей, картинка с кодом - про пароль, а не про SMS
- Спам через формы сайта: капча, ограничение частоты - против частых запросов