Введение

У Google появилась замечательная reCAPTCHA v3. Замечательна она тем, что освобождает пользователя от необходимости тыкать по картинкам в поисках светофоров и пожарных гидрантов, при этом вполне успешно анализирует его действия на сайте и оценивает их по шкале от 0 до 1. Чем выше балл, тем качественней наш бот выше вероятность, что пользователь реальный. Обычно выставляют порог равным 0.5.

А что делать, если хочется добра пользователям сайта, но есть у его владельца некая обеспокоенность, что не пропустит невидимая столь ценного реального пользователя, который может и ведет себя странно и подозрительно, но совсем не бот?

Ответ - последовательно использовать капчу обоих версий - не прошел невидимую проверку - появилась видимая капча!

Что делаем

Тут обозначу основные моменты. Весь код есть по ссылке на github. достаточно переименовать файл config.example.php в config.php и поменять в нем ключи доступа на настоящие.

Для подключения необходимо получить публичные (site key) и приватные (secret key) ключи от обеих версий Google reCAPTCHA на сайте.

Добавляем на страницу скрипт, указав в параметре публичный ключ версии 3

<script src="https://www.google.com/recaptcha/api.js?render=KEY_SITE_V3"></script>

Создаем форму, в которую помимо наших полей добавляем два скрытых для токенов, а также один блок, в который будет добавляться видимая капча. Блоку требуется задать значение атрибута "id".

<input type="hidden" name="captcha_token_v2">
<input type="hidden" name="captcha_token_v3">
<div id="captcha"></div>

Форма у нас будет отправляться по ajax. Пишем обработчик для события подтверждения формы. Тут:

  • form - эта переменная содержит объект формы

  • captcha_key_site_v3 - открытый публичный ключ для капчи 3 версии

  • token - токен, присвоенный пользователю, по нему в дальнейшем будем обращаться в сервис Google и получать те самые баллы


- открытый который проверяет, что определена переменная grecaptcha из библиотеки Google, получает токен и записывает его в скрытое поле, после чего отправляем форму.

form.submit(function (e){
  e.preventDefault()
  if (typeof grecaptcha != 'undefined' && typeof captcha_key_site_v3 != 'undefined') {
    grecaptcha.ready(function () {
      grecaptcha.execute(captcha_key_site_v3, {action: 'submit'}).then(function (token) {
        if(token) {
          form.find('input[name="captcha_token_v3"]').val(token)
          sendForm()
        }
      })
    })
  }
})

Для отправки ипользуется метод sendForm, в котором выполняется такая последовательность действий:

  1. Отправляем ajax запрос с данными формы на сервер

  2. Если на сервере проверка пройдена и данные формы приняты, получаем сообщение об успехе и выводим информацию об этом пользователю

  3. Если произошла ошибка, связанная с версией 3 (не отчечает сервер или недостаточно баллов), то рендерим капчу версии 2

function sendForm() {
                let data = form.serialize()
                $.ajax({
                    type: 'POST',
                    url: form.attr('action'),
                    data: data,
                    dataType: 'json',
                    success: function (response) {
                        if(response.success) {
                            // показываем сообщение об успехе и перезагружаем страницу
                            $('.alert').text(response.text).slideDown()
                            setTimeout(function (){location.reload()}, 4000)
                        } else if(response.error) {
                            // если была ошибка капчи, сбрасываем капчку v2 при наличии
                            if (widgetCaptcha !== false) {
                                grecaptcha.reset(widgetCaptcha)
                            }
                            // если ошибка была в версии v3, показываем видимую капчу v2
                            // widgetCaptcha - идентификатор, т.е. можно рендерить и управлять несколькими штуками
                            if (response.error === 'fall_captcha_v3' && !widgetCaptcha) {
                                widgetCaptcha = grecaptcha.render('captcha', {
                                    'sitekey': captcha_key_site_v2,
                                    'theme': 'dark',
                                    'callback': setTokenV2
                                })
                            }
                        }
                    }
                })
            }

Методу render передаем первым параметром id блока, в который будет помещена капча, вторым - набор параметров:

  • sitekey - публичный ключ капчи версии 2

  • theme - цветовая схема капчи, может принимать значения dark / light

  • callback - имя функции, которая будет вызвана после проверки капчи версии 2

WidgetCaptcha - это переменная, в которой хранится идентификатор капчи, полезно использовать особенно когда на странице несколько форм, для сброса капчи используеся метод grecaptcha.reset(WidgetCaptcha)

В функции-коллбеке setTokenV2 передается токен от второй версии капчи, его мы записываем во второе скрытое поле

function setTokenV2(token) {
  form.find('input[name="captcha_token_v2"]').val(token)
}

В этой же функции можно сразу вызвать sendForm() или предоставить пользователю самостоятельно нажать на кнопку. На этот раз форма улетает уже с двумя токенами, при этом первым проверяется токен версии 2, на всякий случай на стороне сервера по нему тоже получаем инофрмацию от Google, и если все в порядке, возвращаем информацию об успехе в браузер, где и показывем соответстующее сообщение.

Код серверной части в файле ajax.php:

<?php
require_once 'config.php';
session_start();
if(!empty($_POST) && $_POST['sid'] == session_id()) {
    $token_v2 = $_POST['captcha_token_v2'];
    $token_v3 = $_POST['captcha_token_v3'];
    $result = checkCaptcha($token_v2, $token_v3);
    die(json_encode($result, true));
}

function checkCaptcha($token_v2 = false, $token_v3 = false)
{
    // если не передано ни одного токена - возвращаем ошибку
    if (!$token_v3 && !$token_v2) {
        return ['error' => 'fall_captcha'];
    }
    // если дело дошло до капчи второй версии
    elseif ($token_v2) {
        // проверяем информацию по второй версии, если google ответил, что провека успешная - возвращаем успех
        $result = checkCaptchaCurl($token_v2, KEY_SECRET_V2);
        if (!$result['success']) {
            // если проверка провалилась - тоже ошибка
            return ['error' => 'fall_captcha_v2'];
        }
    }
    // если токен второй версии еще не получен, но есть 3, значит проверяем невидимую капчу
    else {
        $result = checkCaptchaCurl($token_v3, KEY_SECRET_V3);
        // проверяем количество очков от 0 до 1. Чем ближе к 1, тем больше вероятности, что это человек
        if ($result['score'] < 1) {
            return ['error' => 'fall_captcha_v3'];
        }
    }
    // возвращаем успех, если проверки пройдены
    $text = 'Your message "' . $_POST['text'] . '" was send to email "' . $_POST['email'] . '" successfully';
    return ['success' => true, 'text' => $text];
}

/**
 * Метод для отправки запроса в google через CURL
 * @param $response
 * @param $secret
 * @return mixed
 */
function checkCaptchaCurl($response, $secret)
{
    $url_data = 'https://www.google.com/recaptcha/api/siteverify' . '?secret=' . $secret . '&response=' . $response . '&remoteip=' . $_SERVER['REMOTE_ADDR'];
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url_data);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    $captcha_res = curl_exec($curl);
    curl_close($curl);
    $captcha_res = json_decode($captcha_res, true);
    return $captcha_res;
}

Надеюсь, кому-то пригодиться в работе, также буду рад комментариям и вопросам.

Комментарии (5)


  1. anonymous
    00.00.0000 00:00


    1. ifap
      12.10.2021 13:33
      +2

      А слепые Google не особо интересны — покупательская способность в среднем ниже, CTR от баннеров и контекстной — никакущий, так что перетопчутся.


    1. berryllium Автор
      12.10.2021 18:50

      Думаю, невидимая капча им как раз в помощь - они же перемещаются в браузере по Tab, заполняют поля с клавиатуры, скрипт гугла наверняка это дело анализирует и распознает человека


  1. Mnemonik
    12.10.2021 14:01
    +4

    Мы так сделали в первой версии, оказалось что против ботов это не то чтобы супер эффективно, - боты зачастую заточены на решение капчи второй версии, причём делают это как-то через фермы с живыми людьми, то есть где-то живые люди решают капчи по публичным ключам с сайта, и производят коды решения. и в итоге скрипты просто игнорируют третью капчу и херачат сразу запросы к апи с решениями второй капчи (сразу с правильными ключами в параметрах). Я смотрю эти скрипты подвержены той же проблеме - достаточно передать им решение второй капчи и всё будет тип топ.

    В итоге наша финальная версия имеет промежуточный стейдж - после того как бэк решает что третья капча так себе была решена, бэк возвращает запрос на рендер второй капчи с nonce и подписью nonce + дата + решение третьей капчи. и решение второй капчи принимается только если вместе с ним поставляется так же решение третьей плюс nonce плюс дата плюс подпись из ответа на третью. И если дата разъезжается минуты на две - машем ручкой.
    Так удалось логически прицепить не решивших третью капчу к решающим вторую и как показала практика это сильно затруднило жизнь всяким умельцам которые покупают на сайтах "решение второй капчи 1 доллар за 10000", потому что это уже просто так не заинтегрировать туда (хотя и можно конечно).

    Конечно боты всё равно регистрируются, но теперь судя по запросам из логов это живые люди которые решают капчи в браузерах. Потому что запрашиваются целиком страницы со стилями и скриптами. Но хотя бы не бесконечный поток запросов прямо к api с корректными решенями второй капчи.


    1. berryllium Автор
      12.10.2021 18:55

      Спасибо за толковый совет!