Перейти к содержимому

Подтверждение телефона кодом из 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, читая значение переключателя из полей формы. Дальше вызывают отправку сообщения или почтовое событие, а хранение кода и счётчик попыток остаются общими.

Можно ли подтверждать номер при заказе от гостя?

Да, механизм тот же: код в хранилище, проверка перед сохранением заказа. Отличие в том, что учётной записи ещё нет, поэтому ключ привязывают к сеансу оформления, а не к идентификатору пользователя.

Где хранить код - в сессии или в базе?

В хранилище ядра со сроком жизни, а в сессии держат только случайный признак сеанса. Сессия обнуляется при смене устройства и при чистке куков, а запись хранилища переживает запрос и истекает сама.

Смежное

Первоисточник