В этой статье я покажу как подключить капчу от Google (reCAPTCHA) на Vue JS и валидировать ответ на сервере (я использую бекенд на Laravel/Lumen в качестве примера, но принцип валидации одинаковый для всех технологий).


Предисловие


Мы будем использовать Invisible reCAPTCHA.
Это невидимая* капча, которую не нужно проходить до тех пор, пока система не посчитает это нужным. Администратор сайта может установить пороговый уровень (Security Preference), при котором система будет требовать от пользователя пройти дополнительную проверку.


*- значок reCAPTCHA все же должен присутствовать на странице.



Невидимая капча на сайте.


Вопрос / Ответ


Перед началом работы я хотел бы ответить на вопросы, которые возникли у меня, когда я начинал работу с Google reCAPTCHA.


В: Сколько стоит использование reCAPTCHA?
О: ReCAPTCHA от Google — это бесплатный инструмент.


В: Зачем нужно еще раз проверять ответ пользователя на бекенде, если он уже прошел капчу на сайте?
О: Когда вы делаете запрос на сервер, вы отправляете что-то наподобие этого:


POST /register 1.1 HTTP
Host: www.example.com
{
    "email:"user@gmail.com", 
    "password": "supersecret", 
    "recaptcha-token":"01ASJASJFZ_AASD3115..."
}

Если не проверять токен капчи на бекенде, боты могут просто спамить запросами БЕЗ этого токена или подменять его на фиктивный.


В: У меня реакт, что мне делать?
О: React JS — отличная вещь. Если вы используете его для разработки сайтов, советую обратить внимание на https://github.com/appleboy/react-recaptcha. Принцип работы очень похож на пример с Vue.


Приступаем к работе


Итак, прежде чем начать готовить, нам нужен список ингредиентов:



Шаг №1: Получаем ключи для использования reCAPTCHA на своем сайте


Нам нужно получить site key и secret key на сайте Google: https://www.google.com/recaptcha/admin#list



В поле label можете писать что угодно.


Далее выберите тип — Invisible reCAPTCHA badge.


Вы можете указать несколько доменов, на которых будете использовать капчу.
В качестве примера я указал yourawesomedomain.com и localhost.


Принимаем условия использования и двигаемся дальше.



Вам выдали два ключа, сохраните их в надежном месте. Мы вернемся к ним позже.


Шаг №2: Фронтенд. Установка и подключение капчи к нашим формам


Для начала, нам нужно добавить этот код в секцию head:


<script src="https://www.google.com/recaptcha/api.js?onload=vueRecaptchaApiLoaded&render=explicit" async defer></script>

Свойство onload сообщит нашему компоненту, что капча готова к использованию.


Дальше я покажу готовый компонент и мы разберем его по частям:


<template>
  <div id="app">
    <div class="container my-4">
      <div class="row justify-content-center">
        <div class="col-md-8">
          <h2 class="text-center mb-4">
            Sign Up Form with Google reCAPTCHA
          </h2>
          <form
              method="post"
              @submit.prevent="validate">
            <div class="form-group">
              <input 
                   type="email" 
                   name="email" 
                   class="form-control" 
                   placeholder="Enter your e-mail address"
                   required />
            </div>
            <div class="form-group">
              <input 
                   type="password" 
                   name="password" 
                   class="form-control" 
                   placeholder="Enter your password"
                   required />
            </div>
            <div class="form-group">
              <vue-recaptcha
                ref="recaptcha"
                size="invisible"
                :sitekey="sitekey"
                @verify="register"
                @expired="onCaptchaExpired"
              />
              <button 
                    type="submit" 
                    class="btn btn-primary btn-block">
                Sign Up
              </button>
            </div>
          </form>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import VueRecaptcha from 'vue-recaptcha'

export default {
  name: 'Register',

  components: { VueRecaptcha },

  data () {
    return {
      email: null,
      password: null,
      sitekey: 'ВАШ SITE KEY'
    }
  },

  methods: {
    register (recaptchaToken) {
      axios.post('https://yourserverurl.com/register', {
        email: this.email,
        password: this.password,
        recaptchaToken: recaptchaToken
      })
    },

    validate () {
      // тут можно добавить проверку на валидацию
      // например, с помощью vee validate
      // если с валидацией наших полей все хорошо, запускаем каптчу
      this.$refs.recaptcha.execute()
    },

    onCaptchaExpired () {
      this.$refs.recaptcha.reset()
    }
  }
}
</script>

Пример компонента с использованием VeeValidate для валидации полей
<template>
  <div id="app">
    <div class="container my-4">
      <div class="row justify-content-center">
        <div class="col-md-8">
          <h2 class="text-center mb-4">
            Sign Up Form with Google reCAPTCHA
          </h2>
          <form
            method="post"
            @submit.prevent="validate">
            <div class="form-group">
              <input
                type="email"
                name="email"
                class="form-control"
                placeholder="Enter your e-mail address"
                v-validate.disable="'required|email'"
                required />
                <div
                  v-show="errors.has('email')"
                  class="invalid-feedback d-block"
                >
                  {{ errors.first('email') }}
                </div>
            </div>
            <div class="form-group">
              <input
                type="password"
                name="password"
                class="form-control"
                placeholder="Enter your password"
                v-validate.disable="'required|min:6|max:32'"
                required />
              <div
                v-show="errors.has('password')"
                class="invalid-feedback d-block"
              >
                {{ errors.first('password') }}
              </div>
            </div>
            <div class="form-group">
              <vue-recaptcha
                ref="recaptcha"
                size="invisible"
                :sitekey="sitekey"
                @verify="register"
                @expired="onCaptchaExpired"
              />
              <button
                type="submit"
                class="btn btn-primary btn-block">
                Sign Up
              </button>
            </div>
          </form>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import VueRecaptcha from 'vue-recaptcha'

export default {
  name: 'Register',

  components: { VueRecaptcha },

  data () {
    return {
      email: null,
      password: null,
      sitekey: 'ВАШ SITE KEY'
    }
  },

  methods: {
    register (recaptchaToken) {
      axios.post('https://yourserverurl.com/register', {
        email: this.email,
        password: this.password,
        recaptchaToken: recaptchaToken
      })
    },

    validate () {
      const self = this
      self.$validator.validateAll().then((result) => {
        if (result) {
          self.$refs.recaptcha.execute()
        }
      })
    },

    onCaptchaExpired () {
      this.$refs.recaptcha.reset()
    }
  }
}
</script>

Начнем с того, что мы имортировали Vue-Recaptcha в наш компонент:


import VueRecaptcha from 'vue-recaptcha'
...
components: { VueRecaptcha },

Дальше мы обьявили свойство sitekey в data() компонента:


data () {
    return {
      ...
      sitekey: 'ВАШ SITE KEY'
    }
  },

Добавляем компонент Vue-Recaptcha в нашу форму:


<vue-recaptcha
    ref="recaptcha"
    size="invisible"
    :sitekey="sitekey"
    @verify="register"
    @expired="onCaptchaExpired"
/>

Метод register будет вызван при успешном прохождении капчи, в то время как expired вызывается при истечении срока действия капчи.


Метод onCaptchaExpired перезапускает капчу:


onCaptchaExpired () {
      this.$refs.recaptcha.reset()
}

Самой форме мы добавляем событие @submit.prevent="validate", которое запускает метод validate при отправке формы.


validate () {
      this.$refs.recaptcha.execute()
}

Процес можно описать так:


  1. Пользователь ввел данные и нажал кнопку Sign Up, вызывается функция validate().
  2. Функция validate() запускает капчу, если пользователь ее успешно проходит, вызывается метод register.

В методе register мы получаем recaptchaToken, который мы должны отправить на сервер вместе с данными, которые ввел пользователь:


register (recaptchaToken) {
      axios.post('https://yourserverurl.com/register', {
        email: this.email,
        password: this.password,
        recaptchaToken: recaptchaToken
      })
}

На этом наша работа с фронтендом закончена. ReCAPTCHA успешно установлена и работает.


P.S. Если вы собираетесь использовать капчу в нескольких компонентах, лучше всего вынести sitekey в .env файл и получать его с помощью process.env:


data () {
    return {
      ...
      sitekey: process.env.VUE_APP_RECAPTCHA_TOKEN
    }
},

Шаг №3: Валидация на бекенде. Пример валидации на Laravel и Lumen


Сделать валидацию ответа на сервере очень просто. Начнем с примера на Laravel.


1) В папке config создайте файл recaptcha.php с таким содержимым:


<?php

return [
    'enabled' => env('RECAPTCHA_ENABLED', true),
    'key'     => env('RECAPTCHA_SITE_KEY'),
    'secret'  => env('RECAPTCHA_SECRET_KEY'),
];

2) После этого, добавьте переменные в .env файл:


RECAPTCHA_ENABLED=FALSE
RECAPTCHA_SITE_KEY=ВАШ_SITE_KEY
RECAPTCHA_SECRET_KEY=ВАШ_SECRET_KEY

3) Установите GuzzleHttp, чтобы иметь возможность отправлять запросы на API гугла:


composer require guzzlehttp/guzzle

4) В контроллере добавьте метод checkRecaptcha:


protected function checkRecaptcha($token, $ip)
{
    $response = (new Client)->post('https://www.google.com/recaptcha/api/siteverify', [
        'form_params' => [
            'secret'   => config('recaptcha.secret'),
            'response' => $token,
            'remoteip' => $ip,
        ],
    ]);
    $response = json_decode((string)$response->getBody(), true);
    return $response['success'];
}

В этом методе мы отправляем наш токен (который мы получили с фронтенда) методом POST на https://www.google.com/recaptcha/api/siteverify


5) В методе register (в вашем случае название может отличатся, это метод на который вы отправляли POST запрос с фронтенда) добавьте следующий код:


if (config('recaptcha.enabled') && !$this->checkRecaptcha($request->recaptcha_token, $request->ip())) {
    return return response()->json([
        'error' => 'Captcha is invalid.',
    ], Response::HTTP_BAD_REQUEST);
}

Все готово к использованию!


  • При обращении к методу register мы получаем email, password и recaptcha_token.
  • Если recaptcha включена (в .env файле значение RECAPTCHA_ENABLED стоит TRUE),
    Laravel отправит запрос на API гугла.
  • Если ответ отрицательный, мы возвращаем ошибку с текстом: Captcha is invalid.
  • Если положительный — продолжаем регистрацию пользователя.


Путешествие токена. Визуальное представление.


Полный код контроллера будет выглядеть так:


<?php

namespace App\Http\Controllers\Users;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use GuzzleHttp\Client;

class UserController extends Controller
{
    protected function checkRecaptcha($token, $ip)
    {
        $response = (new Client)->post('https://www.google.com/recaptcha/api/siteverify', [
            'form_params' => [
                'secret'   => config('recaptcha.secret'),
                'response' => $token,
                'remoteip' => $ip,
            ],
        ]);
        $response = json_decode((string)$response->getBody(), true);
        return $response['success'];
    }

    public function register(Request $request)
    {
        $request->validate([
            'email' => 'required|string|email|unique:users|max:255',
            'password' => 'required|string|max:32|min:6',
            'recaptcha_token' => 'string'
        ]);

        if (config('recaptcha.enabled') && !$this->checkRecaptcha($request->recaptcha_token, $request->ip())) {
            return response()->json([
                'error' => 'Captcha is invalid.',
            ], Response::HTTP_BAD_REQUEST);
        }

        // Капча ОК. Регистрируем пользователя...
    }
}

Валидация ответа на Lumen


На Lumen все делаем так же, как в примере с Laravel, за исключением того, что нам нужно зарегистрировать наш конфиг (recaptcha.php) в bootstrap/app.php:


$app->configure('recaptcha');

Заключение


Итак, в этой статье вы научились использовать Google reCAPTCHA в своем проекте на Vue.


ReCAPTCHA — отличный бесплатный инструмент для защиты своего ресурса от ботов.
С невидимой капчой можно проверять посетителей, не требуя от них никаких действий.


Пример приложения из статьи на codepen


Использованные источники:


  1. https://github.com/DanSnow/vue-recaptcha
  2. https://developers.google.com/recaptcha/docs/invisible
  3. https://developers.google.com/recaptcha/docs/verify
  4. https://security.stackexchange.com/questions/78807/how-does-googles-no-captcha-recaptcha-work

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


  1. Savetchuk Автор
    12.03.2019 03:38

    Если кого-то смущает сообщение в консоли — «reCAPTCHA couldn't find user-provided function: vueRecaptchaApiLoaded», удалите код с секции head и вставьте капчу с помощью JS в методе created():

    created () {
        const $script = document.createElement('script')
        $script.async = true
        $script.src = 'https://www.google.com/recaptcha/api.js?onload=vueRecaptchaApiLoaded&render=explicit'
        document.head.appendChild($script)
      },
    

    Это сообщение означает, что браузер загрузил капчу быстрее, чем наше приложение.
    Компонент все равно определит капчу.


    1. Savetchuk Автор
      12.03.2019 04:22

      Только не забудьте удалять скрипты в методе beforeDestroy (если вы используете роутинг).


  1. Fess_blaga
    12.03.2019 14:39
    +1

    1. Вам не кажется, что в методе register строчка

    const self = this

    явно лишняя? Там (и дальше по коду), обращение к свойствам экземпляра Vue через this вполне отработает.

    axios.post('https://yourserverurl.com/register', {
    email: this.email,
    password: this.password,
    recaptchaToken: recaptchaToken
    })


    2. recaptchaToken в опции data() точно не место)))


    1. Savetchuk Автор
      12.03.2019 14:44

      Спасибо! Хорошо, что заметили.
      Я отредактировал статью.