Подключение reCaptcha v3 и v2 (невидимая и чекбокс) на сайт

Логотип reCaptcha

Как работает капча? С начало добавляем сайт в панели администратора капчи, получаем пару ключей один открытый другой закрытый, открытый используется на сайте он виден всем, закрытый же используется для проверки на сервере. На сайте мы подключаем скрипт капчи, и во время отправки формы с помощью капчи генерируем токен для текущего пользователя, после отправляем этот токен на сервер, где с помощью закрытого ключа этот токен делает запрос на сервер google где происходит проверка и нам приходит ответ является ли этот пользователь ботом. В зависимости от ответа разрешаем пользователю делать действия на сайте(логиниться, регистрироваться и прочие). Сразу же возникает можно ли выполнять проверку токена не на сервере, а в браузере? Ответ: Можно, только смысла в этом нет, это не безопасно и обойти эту проверку просто (проверка всегда должна выполнятся на сервере).

Подключать капчу будем к форме подписки на рассылку. Форма принимает е-мейл, делает запрос на сервер. На сервере е-мейл записывается в базу (теоретически), на клиент приходит ответ, об успешной записи. Рассмотрим наш пример.

HTML:

<form class="subscription" name="subscribe-form-0">
  <label for="0">Email:</label>
  <input id="0" class="mail" name="email" type="text" />
  <input class="send-mail" type="submit" value="send" />
</form>

На форму навешен слушатель submit, который делает запрос на '/.netlify/functions/fake-subscribe' этот экшен находится на сервере, после успешного ответа выводится alert.

JavaScript:

const subscribe_form_0 = document.forms["subscribe-form-0"];

subscribe_form_0.addEventListener("submit", async (e) => {
  e.preventDefault();
  const email = new FormData(e.target).get("email");
  if (email) {
    const response = await fetch("/.netlify/functions/fake-subscribe", {
      body: email,
      method: "POST"
    });
    const data = await response.json();
    alert(data);
  }
});

В качестве сервера используются функции Netlify позволяющие создавать экшены. Этот экшен на самом деле в базу ничего не записывает, а просто возвращает строку с обернутым е-мейлом:

exports.handler = async (event, context) => {
  const email = event.body || null;
  if (email) {
    return {
      statusCode: 200,
      body: JSON.stringify(`user ${email} successfully subscribed`)
    };
  }
};

Итоговый результат:

При вводе е-мейла и нажатии на кнопку Send получаем всплывающие окно об успешной подписке. Однако сейчас форма не защищена от спама и ботов. Начнем с reCaptcha V3

Подключение невидимой reCaptcha v3

Регистрируем сайт в панели администратора. В домены добавляем домен сайта и если вы разрабатываете локально, хост на котором развернут сайт у меня это localhost

Регистрации капчи v3
Регистрации капчи v3

После успешной регистрации станут доступны пара ключей капчи:

Пара ключей
Пара ключей

В отличие от второй версии reCaptcha v3 не нужно проходить ни какие тесты, и выбирать светофоры, капча работает в фоне, и на основе вашего поведения на странице возвращает score от 1 до 0 где, 0 это скорее всего бот, а 1 это человек.

Добавим на страницу следующий скрип с параметром render и значением открытого ключа.

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

Можно заметить что в правом нижнем углу появился бейдж reCaptcha.

Создадим новую форму subscribe_form_1 и добавим обработчик на нее. Вызываем метод ready и передаем в него колбек, что бы быть уверенным что капча доступна. В колбеке вызываем метод execute что бы получить токен. Далее делаем запрос на измененный экшен /.netlify/functions/subscribe-captcha-v3 и теперь помимо emeil, передаем токен.

subscribe_form_1.addEventListener("submit", (e) => {
  e.preventDefault();
  const email = new FormData(e.target).get("email");
  if (email) {
    grecaptcha.ready(async () => {
      const token = await grecaptcha.execute("XXX_OPEN_KEY", { action: "submit" });
      const response = await fetch("/.netlify/functions/subscribe-captcha-v3", {
        body: JSON.stringify({ email, token }),
        method: "POST"
      });
      const data = await response.json();
      alert(data);
    });
  }
});

Осталось сделать проверку на сервере. Проверяем пришел ли нам токен и е-мейл. Затем вызываем функцию isValidCaptcaV3 и передаем в нее токен. Функция делает запрос на API капчи, в который get-параметрами передаем секретный ключ и токет. В ответ нам приходит следующий объект: { success: true, challenge_ts: '2021-01-10T18:52:15Z', hostname: 'localhost', score: 0.9, action: 'submit' } Нам понадобиться два поля, success - являлся ли отправленный токен валидным и score - счет запроса. Мы будем считать что пользователь прошел капчу если его счет больше или равен 0.6. Если пользователь прошел капчу мы возвращаем успешное сообщение.

Серверный экшен:

const secretKey = "XXX_SECRET_KEY";
const api = "https://www.google.com/recaptcha/api/siteverify";

exports.handler = async (event, context) => {
  const { email, token } = JSON.parse(event.body);
  if (email && token) {
    const valid = await isValidCaptcaV3(token);
    if (valid) {
      return {
        statusCode: 200,
        body: JSON.stringify(`user ${email} successfully subscribed`)
      };
    }
  }
};

async function isValidCaptcaV3(token) {
  const url = `${api}?secret=${secretKey}&response=${token}`;
  return fetch(url, { method: "POST" })
    .then((response) => response.json())
    .then((data) => data && data.success && data.score >= 0.6)
    .catch((e) => {
      console.log(e);
      return false;
    });
}

Поздравляю reCaptcha v3 подключена на сайт:

Подключение reCaptcha v2 чекбокс

В панели администратора в типе reCaptcha выбираем флажок "я не робот" так же указываем домен и получаем пару ключей. Капчу v2 можно рендерить автоматически, мы же будем делать это явно. Для этого форму добавим тег div c id recaptcha-checkbox в этом месте мы будем рендерить капчу с чекбоксом.

<form class="subscription" name="subscribe-form-2">
  <label for="2">Email:</label>
  <input id="2" class="mail" name="email" type="text" />
  <input class="send-mail" type="submit" value="send" />
  <div id="recaptcha-checkbox"></div>
</form>

Далее нужно отрендерить капчу. Вызываем метод render в который передаем куда отредерить капчу и открытый ключ. render вернет id капчи это нам нужно по скольку мы используем несколько капч на странице и необходимо понимать к какой обращаться. Затем изменим слушатель формы, в метод getResponse передаем id капчи, в ответ получаем токен, но только в том случае если пользователь нажал на чекбокс. Иначе getResponse ничего не вернет. И если пользователь прошел капчу и ввел email делаем запрос на сервер. На сервер помимо токена и е-мейла передаем какой вид второй капчи мы используем по скольку (я для проверки не видимой и чекбокса я использую один метод). После успешного ответа сервера нужно сбросить капчу иначе можно будет отравить запрос со старым токеном, мы это делаем методом reset и указываем id капчи.

const checkboxCaptcha = document.getElementById("recaptcha-checkbox");
let checkbox_widget_id = null;

grecaptcha.ready(() => {
  checkbox_widget_id = grecaptcha.render(checkboxCaptcha, {
    sitekey: "XXX_OPEN_KEY"
  });
});

subscribe_form_2.addEventListener("submit", async (e) => {
  e.preventDefault();
  const email = new FormData(e.target).get("email");
  const token = grecaptcha.getResponse(checkbox_widget_id);
  if (email && token) {
    const response = await fetch("/.netlify/functions/subscribe-captcha-v2", {
      body: JSON.stringify({ email, token, isCheckbox: true }),
      method: "POST"
    });
    const data = await response.json();
    if (data) {
      grecaptcha.reset(checkbox_widget_id);
      alert(data);
    }
  }
});

На сервере у нас появилась функция isValidCaptcaV2 которая проверяем токен, функция может проверять капчу v2 двух типов. И капча считается успешной если с api google приходить ответ success: true

Экшен /.netlify/functions/subscribe-captcha-v2:

const secretKeyCheckbox = "XXX_SECRET_KEY";
const secretKeyInvisible = "XXX_SECRET_KEY";
const api = "https://www.google.com/recaptcha/api/siteverify";

exports.handler = async (event, context) => {
  const { email, token, isCheckbox } = JSON.parse(event.body);
  if (email && token) {
    const valid = await isValidCaptcaV2(token, isCheckbox);
    if (valid) {
      return {
        statusCode: 200,
        body: JSON.stringify(`user ${email} successfully subscribed`)
      };
    }
  }
};

async function isValidCaptcaV2(token, isCheckbox) {
  const secret = isCheckbox ? secretKeyCheckbox : secretKeyInvisible;
  const url = `${api}?secret=${secret}&response=${token}`;
  return fetch(url, { method: "POST" })
    .then((response) => response.json())
    .then((data) => data && data.success)
    .catch((e) => {
      console.log(e);
      return false;
    });
}

Результат:

Подключение reCaptcha v2 невидимая

В панели администратора в типе reCaptcha выбираем "Невидимый значок reCAPTCHA", получаем пару ключей. Добавляем в форму блок с id recaptcha-invisible в нем будет рендерится капча. Механизм работы невидимой капчи немного отличается, необходимо явно вызывать выполнения капчи.

<form class="subscription" name="subscribe-form-3">
  <label for="3">Email:</label>
  <input id="3" class="mail" name="email" type="text" />
  <input class="send-mail" type="submit" value="send" />
  <div id="recaptcha-invisible"></div>
</form>

Далее рендерем капчу с несколькими параметрами:

sitekey - отрытый ключ,

badge - inline, позволяет позиционировать бейдж с помощью css,

invisible - используется для создания невидимого виджета, привязанного к div и выполняемого программно (наш случай),

callback - функция, которая вызываться при явном выполнение капчи.

Теперь обработчик явно вызывает выполение капчи с помощью метода execute. В результате вызывается колбек mailSubscribe в который передается токен, делается запрос на сервер и после успешного ответа, необходимо сбросить капчу. Серверный экшен остался без изменений.

const invisibleCaptcha = document.getElementById("recaptcha-invisible");
let invisible_widget_id = null;

grecaptcha.ready(() => {
  invisible_widget_id = grecaptcha.render(invisibleCaptcha, {
    sitekey: "XXX_OPEN_KEY",
    badge: "inline",
    callback: mailSubscribe,
    size: "invisible"
  });
});

subscribe_form_3.addEventListener("submit", async (e) => {
  e.preventDefault();
  const email = new FormData(e.target).get("email");
  if (email) {
    grecaptcha.execute(invisible_widget_id);
  }
});

async function mailSubscribe(token) {
  if (token) {
    const email = new FormData(subscribe_form_3).get("email");
    const response = await fetch("/.netlify/functions/subscribe-captcha-v2", {
      body: JSON.stringify({ email, token, isCheckbox: false }),
      method: "POST"
    });
    const data = await response.json();
    if (data) {
      grecaptcha.reset(invisible_widget_id);
      alert(data);
    }
  }
}

Результат:

Так же стоит упомянуть что вы можете скрывать бейджы, но вы должны при этом вставить следующий текст:

This site is protected by reCAPTCHA and the Google
<a href="https://policies.google.com/privacy">Privacy Policy</a> and
<a href="https://policies.google.com/terms">Terms of Service</a> apply.