Создание простой капчи на PHP

Александр Мальцев
Александр Мальцев
23K
18
Создание простой капчи на PHP
Содержание:
  1. Что такое Captcha?
  2. Исходные коды капчи
  3. Верстка формы
  4. Генерация кода капчи и изображения
  5. Скрипт для обновления капчи на форме
  6. Написание обработчика формы
  7. JavaScript для отправки формы на сервер через AJAX
  8. Комментарии

Статья, в которой разберём как написать простую Captcha на PHP. При этом рассмотрим как вариант с использованием сессии, так и куки.

Что такое Captcha?

Captcha (капча) – это некий тест, который человек решает очень легко, а робот – нет (научить компьютер решать его крайне сложно и затруднительно).

Другими словами, основная цель капчи – это определить кем является пользователь: человеком или роботом.

Используется Captcha на сайте для защиты от спама и повышенной нагрузки, которые создают роботы. Капчу можно очень часто встретить в формах регистрации, входа, отправки сообщений, при скачивании файлов и многих других местах.

В большинстве случаев капча отображается как некоторый искаженный или наложенный на фон текст, который посетителю сайта необходимо разобрать и ввести его в некоторое поле. Кроме текста на фоне используется и другие алгоритмы: найти среди множества картинок правильные, собрать пазл, переместить слайдер, нарисовать связь между несколькими картинками и т.д.

Исходные коды капчи

Исходные коды капчи расположены на GitHub: itchief/captcha.

Скриншот формы с капчей:

Форма с капчой

Демо капчи

Процесс разработки капчи представлен в виде следующих этапов:

  • верстка формы;
  • создания файла «captcha.php» для генерация кода капчи и изображения;
  • написание обработчика для формы (файл «process-form.php»);
  • написание JavaScript для отправки формы на сервер через AJAX и обработки ответа.

Верстка формы

Разработку Captcha начнём с создания формы. Для простоты форма будет состоять из капчи и кнопки отправить:

<!-- Сообщение которое будем показывать при успешной отправки формы -->
<div class="form-result d-none">Форма успешно отправлена!</div>

<!-- Форма -->
<form id="form" action="/assets/php/process-form.php" method="post">
  <!-- Капча -->
  <div class="captcha">
    <div class="captcha__image-reload">
      <img class="captcha__image" src="/assets/php/captcha.php" width="132" alt="captcha">
      <button type="button" class="captcha__refresh"></button>
    </div>
    <div class="captcha__group">
      <label for="captcha">Код, изображенный на картинке</label>
      <input type="text" name="captcha" id="captcha">
      <div class="invalid-feedback"></div>
    </div>
  </div>
  <!-- Кнопка "Отправить" -->
  <button type="submit">Отправить</button>
</form>

Генерация кода капчи и изображения

Формирование кода капчи и изображения выполним в файле «captcha.php», который поместим в папку «/assets/php»:

<?php

define('USE_SESSION', true);

// 1. Генерируем код капчи
// 1.1. Устанавливаем символы, из которых будет составляться код капчи
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz';
// 1.2. Количество символов в капче
$length = 6;
// 1.3. Генерируем код
$code = substr(str_shuffle($chars), 0, $length);

if (USE_SESSION) {
  // 2a. Используем сессию
  session_start();
  $_SESSION['captcha'] =  crypt($code, '$1$itchief$7');
  session_destroy();
} else {
  // 2a. Используем куки (время действия 600 секунд)
  $value = crypt($code, '$1$itchief$7');
  $expires = time() + 600;
  setcookie('captcha', $value, $expires, '/', 'test.ru', false, true);
}

// 3. Генерируем изображение
// 3.1. Создаем новое изображение из файла
$image = imagecreatefrompng(__DIR__ . '/files/bg.png');
// 3.2 Устанавливаем размер шрифта в пунктах
$size = 36;
// 3.3. Создаём цвет, который будет использоваться в изображении
$color = imagecolorallocate($image, 66, 182, 66);
// 3.4. Устанавливаем путь к шрифту
$font = __DIR__ . '/files//oswald.ttf';
// 3.5 Задаём угол в градусах
$angle = rand(-10, 10);
// 3.6. Устанавливаем координаты точки для первого символа текста
$x = 56;
$y = 64;
// 3.7. Наносим текст на изображение
imagefttext($image, $size, $angle, $x, $y, $color, $font, $code);
// 3.8 Устанавливаем заголовки
header('Cache-Control: no-store, must-revalidate');
header('Expires: 0');
header('Content-Type: image/png');
// 3.9. Выводим изображение
imagepng($image);
// 3.10. Удаляем изображение
imagedestroy($image);

Генерирование текста капчи выполняется очень просто. Для этого в переменную $chars помещаются символы, из которых она может состоять. Далее с помощью функции str_shuffle() эти символы случайным образом перемешиваются и посредством substr выбирается первые шесть из них.

Сохранении полученной капчи по умолчанию осуществляется в сессионную переменную. Но если хотите в куки, то установите переменной $use_session значение false:

define('USE_SESSION', false);

Если используете протокол HTTPS, то установите шестому аргументу значение true:

setcookie('captcha', $value, $expires, '/', 'test.ru', true, true);

Для отправки капчи клиенту создается изображение, имеющее фон «bg.png», на котором с помощью функции imagefttext() пишется текст капчи.

Скрипт для обновления капчи на форме

Код для обновления капчи при нажатию на кнопку .captcha__refresh:

// функция для обновления капчи
const refreshCaptcha = (target) => {
  const captchaImage = target.closest('.captcha__image-reload').querySelector('.captcha__image');
  captchaImage.src = '/assets/php/captcha.php?r=' + new Date().getUTCMilliseconds();
}
// получение кнопки для обновления капчи
const captchaBtn = document.querySelector('.captcha__refresh');
// запуск функции refreshCaptcha при нажатии на кнопку
captchaBtn.addEventListener('click', (e) => refreshCaptcha(e.target));

Добавление обработчика к кнопке выполняется через addEventListener.

Написание обработчика формы

Для обработки формы создадим файл «process-form.php» в папке «/assets/php/»

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

<?php

// 1a
session_start();
$captcha = $_SESSION['captcha'];
unset($_SESSION['captcha']);
session_destroy();
// 1b
//$captcha = $_COOKIE['captcha'];
//unset($_COOKIE['captcha']);
//setcookie('captcha', '', time() - 3600, '/', 'test.ru', false, true);

$result = ['success' => false];
$code = $_POST['captcha'];

if (empty($code)) {
  $result['errors'][] = ['captcha', 'Пожалуйста введите код!'];
} else {
  $code = crypt(trim($code), '$1$itchief$7');
  $result['success'] = $captcha === $code;
  if (!$result['success']) {
    $result['errors'][] = ['captcha', 'Введенный код не соответствует изображению!'];
  }
}

echo json_encode($result);

В качестве результата будем возвращать JSON. В случае успеха:

{success: true}

В противном случае, success присвоим значение false, а в errors поместим ошибки:

{
  success: false,
  errors: [
    ['captcha', 'Пожалуйста введите код!']
  ]
}

По умолчанию этот файл сравнивает капчу со значением, находящимся в сессии. Если в «captcha.php» сохраняете капчу в куки, то здесь необходимо закомментировать секцию 1a и раскомментировать 1b:

// 1a
//session_start();
//$captcha = $_SESSION['captcha'];
//unset($_SESSION['captcha']);
//session_destroy();
// 1b
$captcha = $_COOKIE['captcha'];
unset($_COOKIE['captcha']);
setcookie('captcha', '', time() - 3600, '/', 'test.ru', false, true);

Если используете протокол HTTPS, то замените шестой аргумент на значение true:

//setcookie('captcha', '', time() - 3600, '/', 'test.ru', true, true);

JavaScript для отправки формы на сервер через AJAX

Код для отправки формы на сервер через AJAX и обработки полученного результата:

const form = document.querySelector('#form');
form.addEventListener('submit', (e) => {
  e.preventDefault();
  try {
    fetch(form.action, {
      method: form.method,
      credentials: 'same-origin',
      body: new FormData(form)
    })
      .then((response) => {
        return response.json();
      })
      .then((data) => {
        document.querySelectorAll('input.is-invalid').forEach((input) => {
          input.classList.remove('is-invalid');
          input.nextElementSibling.textContent = '';
        });
        if (!data.success) {
          refreshCaptcha(form.querySelector('.captcha__refresh'));
          data.errors.forEach(error => {
            console.log(error);
            const input = form.querySelector(`[name="${error[0]}"]`);
            if (input) {
              input.classList.add('is-invalid');
              input.nextElementSibling.textContent = error[1];
            }
          })
        } else {
          form.reset();
          form.querySelector('.captcha__refresh').disabled = true;
          form.querySelector('[type=submit]').disabled = true;
          document.querySelector('.form-result').classList.remove('d-none');
        }
      });
  } catch (error) {
    console.error('Ошибка:', error);
  }
});

В этом коде отправка данных через AJAX выполняется посредством fetch(). Получение данных формы с использованием FormData.

Для отправки и получения cookie посредством fetch() установим:

credentials: 'same-origin',

Если в success находится значение false, то будем помечать поля, которые не прошли валидацию и выводить подсказки:

if (!data.success) {
  refreshCaptcha(form.querySelector('.captcha__refresh'));
  data.errors.forEach(error => {
    console.log(error);
    const input = form.querySelector(`[name="${error[0]}"]`);
    if (input) {
      input.classList.add('is-invalid');
      input.nextElementSibling.textContent = error[1];
    }
  })
}
Капча не прошла проверку

Если в success содержится значение true, то будем очищать поля и выводить сообщение об успешной отправки формы:

form.reset();
form.querySelector('.captcha__refresh').disabled = true;
form.querySelector('[type=submit]').disabled = true;
document.querySelector('.form-result').classList.remove('d-none');
Формы с капчой успешно отправлена

Комментарии:

  1. Елдар
    23 апреля 2021, 12:42
    Добрый день, спасибо за материал, читая комментарии я увидел, что у всех практически заработала капча, чуть выше прочитал, что отображается картинка. Я так же столкнулся с этой проблемой и выполнил Ваши советы, но не помогло. ttf и png в директории files, в html путь к «captcha.php» указан верно. Но картинка не отображается…
    itchief.ru/assets/uploadify/b/1/4/b1494ca8468885107caf44c3887e1c97.png
    itchief.ru/assets/uploadify/c/3/8/c38dbfd45fa6a5601caef367e7131705.png
    1. Александр Мальцев
      24 апреля 2021, 10:19
      Привет! Пожалуйста!
      Для работы капчи нужно поставить веб-сервер, т.к. она создаётся на серверной стороне. Наиболее просто установить веб-сервер — это воспользоваться программой Open Server или каким-то другим WAMP решением.
    2. Dasha
      22 мая 2018, 23:24
      Большое спасибо за урок!
      Наконец поняла, как вставлять в html-форму созданное в php изображение.
      1. Александр Мальцев
        23 мая 2018, 17:59
        Спасибо за оставленный отзыв.
      2. Evgenij
        06 мая 2017, 14:07
        Здравствуйте, Александр! Подскажите пожалуйста, как использовать данную капчу в MODX Revo? С учетом того, что к форме будет применяться FormIt и Ajax? Форм будет несколько. Ооочень нужно, помогите пожалуйста ) я в джиесе и ПыХыПе не очень хорошо разбираюсь, но постараюсь если что )
        1. Александр Мальцев
          08 мая 2017, 17:11
          Здравствуйте. Доработать данный код, для того чтобы с помощью него можно было генерировать несколько капч на одной странице, очень просто.
          Для этого необходимо добавить на страницу с формами (для каждого изображения) при запросе капчи GET параметр:
          captcha.php?id=captcha1
          captcha.php?id=captcha2
          captcha.php?id=captcha3
          И соответственно изменить название полей input, в которые пользователь будет их вводить:
          name="captcha1"
          name="captcha2"
          name="captcha3"
          На сервере при генерации изображения необходимо проверять наличие GET параметра и формировать изображения уже с учётом него:
          $name = 'captcha';
          if (!empty($_GET['id'])) {
            $name = filter_var($_GET['id'], FILTER_SANITIZE_STRING);
          }
          if (USE_SESSION) {
            // 2a. Используем сессию
            session_start();
            $_SESSION[$name] =  crypt($code, '$1$itchief$7');
            session_destroy();
          } else {
            // 2a. Используем куки (время действия 600 секунд)
            $value = crypt($code, '$1$itchief$7');
            $expires = time() + 600;
            setcookie($name, $value, $expires, '/', 'test.ru', false, true);
          }
          Для валидации формы на стороне сервера соответственно это учитываем:
          // 1a
          session_start();
          $captcha = $_SESSION['captcha1'];
          unset($_SESSION['captcha1']);
          session_destroy();
          // 1b
          //$captcha = $_COOKIE['captcha1'];
          //unset($_COOKIE['captcha1']);
          //setcookie('captcha1', '', time() - 3600, '/', 'test.ru', false, true);
          
          $result = ['success' => false];
          $code = $_POST['captcha1'];
          Инструкцию по реализации своей капчи для MODX (для указанных выше компонентов) опубликую немного позже (в виде отдельной статьи).
          1. Evgenij
            13 мая 2017, 06:41
            Здравствуйте! Попробовал поставить… Что та как-то не то получается. Сама капча подставилась, через кнопку обновляется. Но проверка введенного текста в строчке не выполняется. То есть можно ввести все что угодно, и форма все равно отправляется. Не знаю, что там и как. Может ли быть конфликт между валидаторами аякса/формита с проверкой по HTLM5? И как мне кажется, все же если делаем форму через FormIt+Ajax, то нужно как-то по-другому делать… блин. Делаю, кстати, на локальном сервере.
            1. Александр Мальцев
              15 мая 2017, 12:11
              Таким образом, можно использовать несколько капч на странице. Как это сделать для компонента FormIt, напишу в отдельной статье. Кстати опубликую её уже на днях…
              1. Evgenij
                15 мая 2017, 12:29
                Спасибо что держите в курсе событий! )) Ждем с нетерпением! )
            2. Evgenij
              08 мая 2017, 18:09
              Спасибо, Александр, за ответ! Буду пыхтеть стараться, пробовать! А статья по модексу конечно же будет оочень полезной. Это факт!
          2. Михаил
            28 сентября 2016, 19:13
            Здравствуйте. Все перепробовал, не выводится фон капчи с символами… вместо него символическое изображение места картинки и альт капчи. В чем может быть проблема?
            1. Александр Мальцев
              29 сентября 2016, 07:10
              Проверьте есть ли файлы «bg.png» и «oswald.ttf». Они должны находиться в папке files.
              Ещё посмотрите путь в html до «captcha.php».
              1. Михаил
                29 сентября 2016, 10:20
                Спасибо! Помогло.
                1. Александр Мальцев
                  29 сентября 2016, 12:42
                  Отлично! Как работать с путями в PHP можно почитать в этой статье.
            2. Александр
              10 сентября 2016, 16:11
              Здравствуйте. Встроил в форму капчу, все хорошо работает. Но возник один нюанс данные должны отправляться на почту, все это прописано в другом файле php. Подключил через require в файле «process-form.php» и получил такую ситуацию. При вводе капчи и нажатии на кнопку отправить форма отправляется, но на frontend пишет, что неправильно ввели капчу. Если в файле «process-form.php» удалить подключение, то все нормально появляется, как можно решить данную проблему?

              if ($result['success'])
               require 'mail.php';
              }
              1. Александр Мальцев
                12 сентября 2016, 12:50
                Проверьте ответ, который приходит от сервера:
                .then((data) => {
                  console.log(data);
                  //...
              2. Михаил
                12 апреля 2016, 21:43
                Добрый день! Я новичок в этом деле, но у меня получилось всё сделать и всё работает!
                Спасибо за подробный разбор!
                А как можно сделать, чтобы с помощью данной формы происходила регистрация нового пользователя?
                Класс пользователей users уже создал и права им определил! И как чтобы информация из указанной формы переносилась в базу?
                1. Александр Мальцев
                  13 апреля 2016, 16:00
                  В файле «process-form.php» добавить вставку переданных данных (нового пользователя) в базу данных. Но не забудьте также реализовать фильтрацию полей, проверку значений полей установленным требованиям, уникальность логина, email адреса и т.д.
                  Начать можно делать так:
                  // сведения, необходимые для подключения к базе данных
                  $servername = "localhost";
                  $username = "username";
                  $password = "password";
                  $dbname = "nameDatabase";
                  // Создаём соединение
                  $conn = mysqli_connect($servername, $username, $password, $dbname);
                  // Проверяем соединение
                  if (!$conn) {
                    die("Ошибка соединения: " . mysqli_connect_error());
                  }
                  // Добавляем данные в таблицу tableName
                  var login = $_POST["login"];
                  var name = $_POST["name"];
                  var email = $_POST["email"];
                  $sql = "INSERT INTO tableName(login, name, email) VALUES ('$login', '$name', '$email')";
                  
                  if (mysqli_query($conn, $sql)) {
                    echo "Новый пользователь был успешно добавлен";
                  } else {
                    echo "Ошибка: " . $sql . mysqli_error($conn);
                  }
                  
                  mysqli_close($conn);
                Войдите, пожалуйста, в аккаунт, чтобы оставить комментарий.