Почему форма с reCAPTCHA v3 работает, если отправить её быстро, но выдает ошибку, если пользователь не спешит? Всё дело в том, что токен уже истек. Если вы искали, как решить проблему с таймаутом Google v3 reCAPTCHA, то решение кроется не столько в настройках сервера, сколько в том, когда именно ваш JavaScript запрашивает у Google токен.
Краткий ответ: Токен reCAPTCHA v3 действителен всего две минуты, и Google принимает его только один раз. Большинство руководств генерируют токен при загрузке страницы, поэтому пользователи, которые заполняют форму медленно, отправляют уже просроченный токен. Вместо этого вызывайте grecaptcha.execute() внутри обработчика отправки и проверяйте токен на сервере ровно один раз.
Прежде чем начать, вам понадобятся:
- PHP с включенным расширением cURL (в выводе php -m должен присутствовать curl )
- Ключ сайта (site key) и секретный ключ (secret key) v3 из консоли администратора reCAPTCHA, с добавленным вашим доменом (также добавьте localhost , если тестируете локально)
- Доступ к файлам проекта и, в случае с XAMPP, к php.ini
Всё, что описано ниже, рассматривается на одном примере: простая контактная форма на чистом PHP, разделенная на четыре файла.
contact/ ├── config.php # keys and threshold ├── recaptcha.php # verification function ├── contact.php # the form └── send.php # handles the POST
Почему токен reCAPTCHA v3 истекает до отправки формы
Этот момент часто вызывает путаницу, поэтому в нем стоит разобраться до изменения какого-либо кода. Двухминутный отсчет начинается тогда, когда grecaptcha.execute() возвращает токен. Он не запускается в момент нажатия пользователем кнопки отправки.
Вот шаблон, который копирует большинство разработчиков:
grecaptcha.ready(function () {
grecaptcha.execute('SITE_KEY', { action: 'contact' }).then(function (token) {
document.getElementById('g-recaptcha-response').value = token;
});
});
Этот код выполняется сразу при загрузке страницы. Пользователь, который читает страницу, пишет сообщение и отправляет форму через три минуты, отправляет уже недействительный токен. Google отвечает ошибкой timeout-or-duplicate , и пользователь видит сообщение «ошибка проверки», хотя он не сделал ничего плохого.
Решение заключается в том, чтобы изменить момент запроса токена.
Как решить проблему с таймаутом Google v3 reCAPTCHA: пошаговое руководство
Шаг 1: Добавьте ключи в config.php
<?php // contact/config.php const RECAPTCHA_SITE_KEY = 'your-site-key'; const RECAPTCHA_SECRET = 'your-secret-key'; const RECAPTCHA_MIN_SCORE = 0.5;
Не добавляйте этот файл в git или считывайте значения из переменных окружения. Документация Google рекомендует использовать 0.5 в качестве начального порогового значения. Скорректируйте его, когда увидите оценки (scores), которые получает ваш реальный трафик.
Шаг 2: Напишите функцию проверки в recaptcha.php
<?php
// contact/recaptcha.php
function verify_recaptcha(string $token, string $expectedAction): array
{
if ($token === '') {
return ['success' => false, 'error-codes' => ['missing-input-response']];
}
$ch = curl_init('https://www.google.com/recaptcha/api/siteverify');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'secret' => RECAPTCHA_SECRET,
'response' => $token,
'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '',
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
if ($body === false) {
return ['success' => false, 'error-codes' => ['curl: ' . curl_error($ch)]];
}
$result = json_decode($body, true) ?? ['success' => false, 'error-codes' => ['bad-json']];
if (($result['success'] ?? false)
&& (($result['action'] ?? '') !== $expectedAction
|| ($result['score'] ?? 0) < RECAPTCHA_MIN_SCORE)) {
$result['success'] = false;
$result['error-codes'][] = 'low-score-or-wrong-action';
}
return $result;
}
Почему стоит использовать cURL вместо file_get_contents() ? Многие хостинги отключают allow_url_fopen , а с cURL вы получаете понятное сообщение об ошибке, если запрос не удался.
Шаг 3: Запрашивайте токен в момент отправки в contact.php
<?php require __DIR__ . '/config.php'; ?>
<form id="contact-form" method="POST" action="send.php">
<input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response">
<input type="text" name="name" required>
<textarea name="message" required></textarea>
<button type="submit" id="contact-submit">Send</button>
</form>
<script src="https://www.google.com/recaptcha/api.js?render=<?= htmlspecialchars(RECAPTCHA_SITE_KEY) ?>"></script>
<script>
const SITE_KEY = '<?= htmlspecialchars(RECAPTCHA_SITE_KEY) ?>';
document.getElementById('contact-form').addEventListener('submit', function (e) {
e.preventDefault();
const form = this;
document.getElementById('contact-submit').disabled = true;
grecaptcha.ready(function () {
grecaptcha.execute(SITE_KEY, { action: 'contact' }).then(function (token) {
document.getElementById('g-recaptcha-response').value = token;
form.submit(); // native submit, won't fire this listener again
});
});
});
</script>
Теперь каждый токен создается за мгновение до отправки. Отключение кнопки также предотвращает двойные клики, которые иначе привели бы к отправке одного и того же токена дважды.
Шаг 4: Выполните проверку один раз в send.php
<?php
// contact/send.php
require __DIR__ . '/config.php';
require __DIR__ . '/recaptcha.php';
$result = verify_recaptcha($_POST['g-recaptcha-response'] ?? '', 'contact');
error_log('recaptcha: ' . json_encode($result));
if (!$result['success']) {
http_response_code(422);
exit('Verification failed. Please go back and try again.');
}
// Safe to process the message here.
echo 'Thanks, your message was sent.';
Вызывайте verify_recaptcha() только в одном месте. Повторный вызов с тем же токеном всегда завершается ошибкой, и на сервере это выглядит точно так же, как таймаут. В статье о таймаутах и дубликатах проблема дублирования рассмотрена более подробно.
Варианты: AJAX-формы, несколько форм и обновление токена
Формы на AJAX или fetch()
Здесь правило то же самое: запрашивайте токен непосредственно перед отправкой запроса и запрашивайте новый для каждой попытки повтора.
async function sendContact(form) {
const token = await new Promise(function (resolve) {
grecaptcha.ready(function () {
grecaptcha.execute(SITE_KEY, { action: 'contact' }).then(resolve);
});
});
const data = new FormData(form);
data.set('g-recaptcha-response', token);
return fetch('send.php', { method: 'POST', body: data });
}
Если ваш код повторяет неудачные запросы, вызывайте sendContact() заново, а не отправляйте повторно старый FormData .
Несколько форм на одной странице
Назначьте каждой форме собственный обработчик событий и свое имя action ( contact , newsletter и так далее). Затем передавайте соответствующее имя в verify_recaptcha() . Таким образом, токен, сгенерированный для формы подписки, нельзя будет использовать для прохождения проверки в контактной форме.
Обновление токена по таймеру
Некоторые разработчики обновляют токен каждые 90 секунд, чтобы в скрытом поле всегда находился свежий токен:
function refreshToken() {
grecaptcha.execute(SITE_KEY, { action: 'contact' }).then(function (token) {
document.getElementById('g-recaptcha-response').value = token;
});
}
grecaptcha.ready(function () {
refreshToken();
setInterval(refreshToken, 90000);
});
Это работает, и это полезно, когда плагин требует наличия токена в поле до отправки. Однако, по моему мнению, это худший выбор. Код постоянно обращается к Google, пока вкладка открыта, а браузеры часто замедляют таймеры на неактивных вкладках. В результате пользователь, вернувшийся к старой вкладке, все равно может отправить просроченный токен. Всегда используйте генерацию в момент отправки, когда это позволяет ваш код.
Проверка работоспособности исправления таймаута
- Откройте contact.php , заполните форму и подождите не менее трех минут перед отправкой. Это тот самый сценарий, который раньше приводил к ошибке.
- Отправьте форму, затем откройте лог ошибок PHP ( C:\xampp\php\logs\php_error_log в настройках XAMPP по умолчанию или там, куда указывает error_log на вашем сервере).
- Найдите строку recaptcha: . Вы должны увидеть "success":true , ожидаемое значение "action":"contact" и значение challenge_ts , отличающееся от времени запроса всего на секунду или две.
Если значение challenge_ts старше времени запроса на несколько минут, значит, токен по-прежнему генерируется слишком рано. Убедитесь, что в вашем JavaScript не осталось вызовов execute() , выполняющихся при загрузке страницы.
Частые ошибки при настройке reCAPTCHA v3 в PHP
«form.submit is not a function»
В вашей форме есть поле или кнопка с именем name="submit" , которое перекрывает встроенный метод формы submit() . Переименуйте поле или вызовите HTMLFormElement.prototype.submit.call(form) .
missing-input-response
Сервер получил пустой токен. Либо атрибут name скрытого инпута задан неверно (ожидается g-recaptcha-response ), либо форма была отправлена до того, как промис завершился (например, из-за вызова jQuery $(form).submit() , который повторно запустил другие ваши обработчики).
invalid-input-secret
Секретный ключ указан неверно или был скопирован с лишним пробелом. Скопируйте его заново из консоли администратора.
«Invalid site key or not loaded in api.js»
Ключ сайта не соответствует ключу в URL render= или текущий домен отсутствует в списке разрешенных доменов для этого ключа.
curl: SSL certificate problem: unable to get local issuer certificate
Это частая проблема на XAMPP для Windows. PHP не может найти набор сертификатов CA (CA bundle). Скачайте cacert.pem с официального сайта cURL, сохраните его в надежном месте, например в C:\xampp\php\extras\ssl\cacert.pem , затем укажите путь к нему в php.ini и перезапустите Apache:
curl.cainfo = "C:\xampp\php\extras\ssl\cacert.pem" openssl.cafile = "C:\xampp\php\extras\ssl\cacert.pem"
Не пытайтесь «исправить» эту ошибку, отключая CURLOPT_SSL_VERIFYPEER . Это убирает ошибку за счет отключения проверки безопасности, что на самом деле не является решением.
Откат изменений в случае сбоя
Прежде чем заменять старый код формы, сделайте коммит или сохраните копию файлов contact.php и send.php . Если новая версия вызовет проблемы на продакшене, вы сможете восстановить старые файлы. Для быстрого экстренного отключения добавьте const RECAPTCHA_ENABLED = true; в config.php и оберните проверку в send.php с помощью if (RECAPTCHA_ENABLED) { ... } . Установка значения false отключает проверку без необходимости деплоя. Только помните, что пока проверка отключена, форма не защищена от ботов.
Часто задаваемые вопросы (FAQ)
Сколько времени действителен токен Google reCAPTCHA v3?
Две минуты после того, как метод grecaptcha.execute() вернет его. Кроме того, его можно проверить только один раз. По истечении любого из этих лимитов Google возвращает timeout-or-duplicate .
Можно ли увеличить время жизни токена reCAPTCHA v3?
Нет. Двухминутный срок действия установлен компанией Google, и в консоли администратора или API нет настроек, позволяющих его изменить. Эту проблему обходят путем генерации токена в нужный момент, а не за счет продления времени его жизни.
Почему reCAPTCHA v3 выдает ошибку только у некоторых пользователей?
Обычно это пользователи, которые заполняют форму дольше двух минут или нажимают кнопку отправки дважды. Еще одной причиной могут быть низкие оценки (scores). Записывайте полный ответ в лог, чтобы отличать просроченный токен от низкой оценки.
Так как же решить проблему с таймаутом Google v3 reCAPTCHA в одной строке?
Перенесите вызов grecaptcha.execute() в обработчик отправки формы, чтобы каждый токен создавался непосредственно перед отправкой, и проверяйте его один раз на сервере. Это решит проблему медленных пользователей, двойных кликов и повторных попыток отправки без использования таймеров или изменений на стороне сервера.
Комментарии (0)
Пока нет комментариев — будьте первым.