Впровадження SMS-аутентифікації з телефонним номером для веб-сайтів
Аутентифікація через SMS-код (OTP) — стандарт для російського ринку в e-commerce, сервісах доставки, фінтеку. Користувач вводить номер телефону, отримує 4–6 значний код в SMS, вводить його — і авторизований. Пароль при цьому або відсутній, або встановлюється окремо після першого входу.
SMS-провайдери для Росії та СНГ
| Провайдер | Особливості |
|---|---|
| SMSC.ru | Популярний, є HTTP API і SMPP |
| SMS.ru | Простий API, хорошою доставляємість |
| Exolve (МТС) | Оператор-рівня, віртуальні номери |
| Infobip | Міжнародний, дорогий, надійний |
| Twilio | Міжнародний, недоступен в РФ без VPN |
| FirebaseSMS | Для мобільних додатків, не для веба |
Для більшості веб-проектів у Росії — SMSC.ru або SMS.ru.
Архітектура флоу
1. POST /auth/phone/send-code { phone: "+79001234567" }
→ валідація номера
→ генерування OTP
→ збереження hash(OTP) у Redis з TTL 5 хв
→ відправка SMS
→ відповідь: { expires_in: 300 }
2. POST /auth/phone/verify { phone: "...", code: "123456" }
→ перевірка OTP з Redis
→ створення/пошук користувача
→ видача сесії або JWT
Генерування та зберігання OTP
class PhoneOtpService
{
public function sendOtp(string $phone): int
{
$this->checkRateLimit($phone);
$code = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
// Зберігаємо хеш, не сам код
Cache::put(
"phone_otp:{$phone}",
[
'hash' => hash('sha256', $code),
'attempts' => 0,
],
now()->addMinutes(5)
);
$this->smsProvider->send($phone, "Ваш код: {$code}");
return 300; // expires_in seconds
}
public function verifyOtp(string $phone, string $code): bool
{
$data = Cache::get("phone_otp:{$phone}");
if (!$data) {
throw new OtpExpiredException();
}
// Обмеження спроб
if ($data['attempts'] >= 3) {
Cache::forget("phone_otp:{$phone}");
throw new OtpAttemptsExceededException();
}
if (!hash_equals($data['hash'], hash('sha256', $code))) {
Cache::put("phone_otp:{$phone}", array_merge($data, [
'attempts' => $data['attempts'] + 1,
]), now()->addMinutes(5));
return false;
}
Cache::forget("phone_otp:{$phone}");
return true;
}
}
Rate Limiting
// Не більше 3 SMS на годину з одного номера
RateLimiter::for('sms-otp', function (Request $request) {
return [
Limit::perHour(3)->by('phone:' . $request->phone),
Limit::perMinute(1)->by('phone:' . $request->phone),
];
});
Нормалізація номера телефону
use libphonenumber\PhoneNumberUtil;
$phoneUtil = PhoneNumberUtil::getInstance();
$parsed = $phoneUtil->parse($rawPhone, 'RU');
if (!$phoneUtil->isValidNumber($parsed)) {
throw new InvalidPhoneNumberException();
}
$normalized = $phoneUtil->format($parsed, \libphonenumber\PhoneNumberFormat::E164);
// +79001234567
Бібліотека giggsey/libphonenumber-for-php — портування Google libphonenumber на PHP.
Інтеграція з SMSC.ru
class SmscProvider implements SmsProviderInterface
{
public function send(string $phone, string $text): void
{
$response = Http::get('https://smsc.ru/sys/send.php', [
'login' => config('sms.smsc_login'),
'psw' => config('sms.smsc_password'),
'phones' => $phone,
'mes' => $text,
'fmt' => 3, // JSON
]);
$data = $response->json();
if (isset($data['error'])) {
Log::error('SMSC error', ['code' => $data['error_code'], 'message' => $data['error']]);
throw new SmsDeliveryException($data['error']);
}
}
}
Створення користувача при першому вході
public function authenticate(string $phone): User
{
return User::firstOrCreate(
['phone' => $phone],
[
'phone_verified_at' => now(),
'name' => 'Користувач',
]
);
}
UX-деталі
- Показувати таймер зворотного відліку до можливості повторної відправки
- Автофокус на поле коду після відправки
- Автосабмит при вводі останної цифри (якщо 6-значний код)
- Кнопка «Змінити номер» — можливість повернутися назад
Часова розкладка робіт
| Етап | Час |
|---|---|
| OTP-сервіс + Redis | 1 день |
| Інтеграція з SMS-провайдером | 0.5 дня |
| API еndpoints + rate limiting | 0.5 дня |
| Frontend флоу (форма + таймер) | 1 день |
| Тести + edge cases | 1 день |
Разом: 4–5 днів.







