Капча в форме Laravel: разбор частых ошибок новичков
Как подключить Google reCAPTCHA v2 к форме на Laravel 11 без пакетов-обёрток и почему форма то молча пропускает ботов, то ругается на «валидную» капчу у живых людей.

В конце вы получите форму на Laravel 11, которая отклоняет отправку без пройденной Google reCAPTCHA v2 и показывает пользователю понятную ошибку. Займёт это минут 20, если у вас уже есть работающий проект и аккаунт Google. Всё делаем без сторонних пакетов-обёрток — только встроенный HTTP-клиент и валидатор Laravel, чтобы вы понимали, где именно всё ломается, а не искали ошибку внутри чужого кода.
Что нужно иметь до начала
- Laravel 11.x и PHP 8.2+ (проверьте командой
php artisan --version). - Аккаунт Google для регистрации сайта в reCAPTCHA.
- Доступ к файлу
.envпроекта и возможность перезапустить кеш конфигурации. - Форма, которую отправляете обычным POST-запросом с CSRF-токеном (в этом гайде — форма обратной связи).
Мы разбираем reCAPTCHA v2 (чекбокс «Я не робот»). Для v3 и других провайдеров логика проверки отличается, здесь их не трогаем.
Шаг 1. Получите ключи в консоли reCAPTCHA
Зайдите в консоль администратора reCAPTCHA и зарегистрируйте сайт. Выберите тип reCAPTCHA v2 → «Флажок «Я не робот»». В поле доменов добавьте домен сайта; для локальной разработки добавьте localhost и 127.0.0.1 отдельными строками.
После регистрации вы увидите две строки: Site Key (ключ сайта, публичный) и Secret Key (секретный ключ, серверный). Не перепутайте их местами — это ошибка номер один, к ней вернёмся.
Проверка: на странице появились обе строки, каждая начинается с набора букв и цифр без пробелов.
Шаг 2. Пропишите ключи в .env и config
Добавьте в файл .env:
RECAPTCHA_SITE_KEY=ваш_site_key
RECAPTCHA_SECRET_KEY=ваш_secret_key
Никогда не обращайтесь к env() напрямую из контроллера или Blade — при кешировании конфигурации функция env() вернёт null. Вместо этого пробросьте значения через файл конфигурации. Откройте config/services.php и добавьте секцию:
'recaptcha' => [
'site_key' => env('RECAPTCHA_SITE_KEY'),
'secret_key' => env('RECAPTCHA_SECRET_KEY'),
],
Теперь в коде значения берутся так: config('services.recaptcha.site_key').
Проверка: выполните php artisan tinker и наберите config('services.recaptcha.secret_key') — должна вернуться ваша строка, а не null.
Шаг 3. Вставьте виджет в Blade-шаблон
Подключите скрипт reCAPTCHA перед закрывающим </body> и добавьте контейнер виджета внутрь формы:
<form method="POST" action="{{ route('contact.store') }}">
@csrf
<input type="email" name="email" value="{{ old('email') }}">
@error('email') <span>{{ $message }}</span> @enderror
<textarea name="message">{{ old('message') }}</textarea>
<div class="g-recaptcha" data-sitekey="{{ config('services.recaptcha.site_key') }}"></div>
@error('g-recaptcha-response') <span>{{ $message }}</span> @enderror
<button type="submit">Отправить</button>
</form>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
При успешном прохождении капчи Google подставит в форму скрытое поле с именем g-recaptcha-response — именно его вы будете проверять на сервере.
Проверка: откройте форму в браузере — появился чекбокс «Я не робот». Если вместо него пустое место или ошибка «Invalid domain», значит домен не добавлен в консоли (шаг 1).
Шаг 4. Проверьте капчу на сервере
Валидировать капчу нужно на бэкенде. Проверка на клиенте ничего не значит — бот отправит POST напрямую, минуя JavaScript. Сделайте кастомное правило валидации, чтобы капча проверялась вместе с остальными полями.
В контроллере метод store:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
public function store(Request $request)
{
$request->validate([
'email' => ['required', 'email'],
'message' => ['required', 'string', 'max:2000'],
'g-recaptcha-response' => ['required', function ($attribute, $value, $fail) {
$response = Http::asForm()->post(
'https://www.google.com/recaptcha/api/siteverify',
[
'secret' => config('services.recaptcha.secret_key'),
'response' => $value,
'remoteip' => request()->ip(),
]
);
// Google возвращает поле success: true/false
if (! $response->json('success')) {
$fail('Подтвердите, что вы не робот.');
}
}],
], [
'g-recaptcha-response.required' => 'Пройдите проверку капчи.',
]);
// ...сохранение или отправка письма
return back()->with('status', 'Сообщение отправлено.');
}
Обратите внимание на имя поля в правилах: 'g-recaptcha-response' — с дефисами, ровно так его называет виджет Google. Если написать g_recaptcha_response, правило никогда не сработает, потому что такого поля в запросе нет.
Проверка: отправьте форму без клика по чекбоксу — увидите «Пройдите проверку капчи». Отправьте с пройденной капчей — форма пройдёт.
Клиентская капча — это вежливость перед пользователем. Реальную защиту даёт только серверная проверка siteverify. Всё, что происходит в браузере, бот при желании обходит одним curl-запросом.
Если не получилось: типичные ошибки
Разбор реальных сообщений, которые вы увидите в ответе от Google или в логах Laravel.
- Форма молча пропускает ботов. Скорее всего вы проверяете капчу только в JavaScript или вообще не добавили правило для
g-recaptcha-response. Проверяйте на сервере — см. шаг 4. - error-codes: ["invalid-input-secret"] в ответе. В запрос
siteverifyуходит Site Key вместо Secret Key. Убедитесь, чтоsecretберёт именноsecret_key. - error-codes: ["missing-input-response"]. Поле
g-recaptcha-responseне дошло до сервера. Частая причина — форма отправляется через AJAX, и вы не собрали значение виджета в тело запроса. - config('services.recaptcha.secret_key') возвращает null. Вы закешировали конфиг раньше, чем добавили ключи. Выполните
php artisan config:clear(или зановоphp artisan config:cacheпосле правок). - timeout-or-duplicate в ответе. Токен капчи одноразовый и живёт около двух минут. Пользователь долго заполнял форму или отправил её повторно после ошибки валидации других полей — токен уже сгорел. Решается перезагрузкой виджета на фронтенде после неудачной отправки.
Про AJAX-формы и повторную отправку
Проблема timeout-or-duplicate — самая коварная. Сценарий: пользователь прошёл капчу, но ошибся в email. Laravel вернул ошибку валидации, страница перезагрузилась через back(), а токен уже недействителен — второй submit провалится по капче, хотя человек живой.
Два рабочих выхода:
- Отправлять форму через AJAX (fetch), не перезагружая страницу, и вызывать сброс виджета только после реальной неудачи капчи.
- При серверной ошибке валидации других полей возвращать пользователя на форму с уже пройденной капчей — то есть проверять капчу последней и не терять токен. На практике проще перерисовать виджет: у объекта
grecaptchaесть метод сброса, актуальное имя сверьте в документации reCAPTCHA v2.
Что делать дальше
Форма защищена. Дальше стоит вынести логику проверки капчи в отдельное правило (класс, реализующий контракт правил валидации Laravel), чтобы не дублировать замыкание в каждом контроллере. Настройте таймаут HTTP-клиента: Http::timeout(5) — если Google не ответит, форма не должна висеть. И добавьте логирование поля error-codes из ответа siteverify, чтобы отличать атаку ботов от проблем на своей стороне.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: reCAPTCHA v2: Displaying and verifying — Google Developers, HTTP Client — Laravel Documentation
Частые вопросы
Работает ли этот код на Laravel 10 или 12?
Почему config('services.recaptcha.secret_key') возвращает null, хотя ключ в .env есть?
Можно ли обойтись без кастомного правила и проверять капчу отдельным if?
Не сработало: получаю missing-input-response при отправке через AJAX. В чём дело?
Нужно ли передавать remoteip в запрос siteverify?
Капча проходит у меня, но падает на проде. Что проверить первым?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.