Капча для codeigniter 4

Добрый день! Несмотря на заголовок статьи, в ней будут представлены общие методы и функции, которые я использовал для создания своей капчи, которые можно применить и в других фреймворках с минимальными правками. Некоторые функции и подходы основываются на материалах поста Разработка CAPTCHA своими руками.

Введение


Для работы с изображениями необходимо проверить наличие GD библиотеки в PHP. Это можно сделать с помощью функции gd_info(). В представленных примерах я использую версию 2.1.0 и PHP 7.4.3, что в данном случае не обязательно, поскольку функции PHP7 не используются.

Логика


Какую капчу я хочу видеть? Такую, которая поможет мне уменьшить число запросов к серверу при авторизации в моем сайте с codeigniter 4.

Для реализации картинка с кодом будет генерироваться исключительно на стороне сервера, сохранятся во временную папку, кодироваться в base64 и возвращаться пользователю.

Разработка


Перед отрисовкой картинки пишем метод генерации кода.

public function generate_code() {
        srand((float) microtime() * 1000000);
        $chars = 'ABDEFHKNRSTYZabdefhknrstyz23456789'; // исходный набор символов
        $length = rand(5, 7); // длина капчи
        $numChars = strlen($chars); 
        $str = '';
        for ($i = 0; $i < $length; $i++) {
            $str .= $chars[rand(0, $numChars - 1)];
        }
        $array_mix = preg_split('//', $str, -1, PREG_SPLIT_NO_EMPTY);
        shuffle($array_mix);
        delete_cookie('cap'); // удаляем кук, если он есть, чтобы можно было повторно использовать метод
        set_cookie('cap', md5(implode("", $array_mix)), self::$_code_time); // записываем в кук код в md5 на время _code_time
        return implode("", $array_mix);
    }

Замечу, что в дальнейшем я буду использовать различные шрифты для вывода символов, потому надо обратить внимание на исходный набор знаков, или на сами шрифты, чтобы избежать проблем такими символами как «Z» и «z», «X» и «x», «I» и «l» и т.д., поскольку искажение картинки может сделать ввод капчи проблематичным.

Объявляю необходимые в дальнейшем поля.

public static $width = 220; // ширина картинки в пикселях
public static $height = 120; // высота картинки
public static $fonts_num = 4; // число шрифтов в папке /public/fonts/
private static $_code_time = 180; // время жизни куков в секундах.

Готовлю некоторые методы для генерации фонов и шумов (полный листинг в конце).

/**
     * Добавляет линии на изображение.
     *
     * $mode == "parallel", рисует параллельные горизонтальные и вертикальные линии
     * $max — максимальное число линий
     */
private function _add_line($img, $mode = '', $max = 100) {
    for ($i = 0; $i < rand(0, $max); $i++) {
        $color = imagecolorallocate($img, rand(80, 150), rand(80, 150), rand(80, 150));
        if ($mode === 'parallel') {
            $r1 = rand(0, self::$width);
            $r2 = rand(0, self::$width);
            imageline($img, $r1, $r1, $r2, $r1, $color);
            imageline($img, $r1, $r2, $r1, rand(0, 220), $color);
        } else {
            imageline($img, rand(0, self::$width), rand(0, self::$width), rand(0, self::$width), rand(0, self::$width), $color);
        }
    }
}

private function _add_poly($img) { // рисуем случайные многоугольники
    $points = [];
    for ($i = 0; $i < 10; $i++) {
        array_push($points, rand(0, self::$width * 2));
    }
    $color = imagecolorallocate($img, rand(80, 190), rand(80, 190), rand(80, 190));
    imageFilledPolygon($img, $points, 5, $color);
}

/**
     * Добавляет искажение на изображение.
     *
     * $xn и $yn определяют направление смещения цвета пикселя. Если они случайны, искажение приобретает вид, похожий на шум. Если статические, то диапазон искажаемых пикселей "вытягивается".
     * $mode определяет используются ли случайные значения для смещения или определенные ('normal').
     */
private function _set_glitch_color($image, $xn = 0, $yn = 0, $mode = 'normal') {
    $start = rand(self::$height / 2, self::$height / 2 - self::$height / 4);
    $finish = $start + rand(5, 15);
    for ($x = 0; $x < self::$width - 1; $x++) {
        for ($y = 0; $y < self::$height - 1; $y++) {
            if ($mode != 'normal') {
                $xn = rand(0, 1);
                $yn = rand(0, 1);
            } else {
                $finish = $start + 3;
            }
            if ($y > $start && $y < $finish) {
                imagesetpixel($image, $x + $xn, $y + $yn, imagecolorat($image, $x, $y));
            }
        }
    }
}

Почти готово. Пишем секретный код на картинке.

private function _add_text($img, $text) {
    $x = rand(10,20); // стартовый отступ по X для первой буквы.
    for ($i = 0; $i < strlen($text); $i++) {
        $text_color = imagecolorallocate($img, rand(150, 250), rand(150, 250), rand(150, 250));
        imagettftext($img, rand(35, 40), rand(0, 10) - rand(0, 10), $x, rand(55, 95), $text_color, 'fonts/' . rand(1, self::$fonts_num) . ".ttf", $text[$i]); // выбирает для каждой буквы шрифт от 1.ttf до *self::$fonts_num*.ttf
        $x += rand(25, 35);
    }
}

Объединяем готовые методы для создания картинки и проверки кода.

public function img_code($code) {
    $image = imagecreatetruecolor(self::$width, self::$height);
    imageantialias($image, true);
    $rand_color = imagecolorallocate($image, rand(50, 120), rand(50, 120), rand(50, 120));
    imagefilledrectangle($image, 0, 0, self::$width, self::$height, $rand_color);
    $this->_add_rand_bg($image); // случайный фон картинке
    $this->_add_text($image, $code); 
    $this->_add_glitch($image, 'normal');
    $this->_add_glitch($image, 'boom');
    $this->_add_line($image, 'rand', 200); // дополнительные линии поверх кода
    $file = 'temp/' . md5($code) . ".png"; 
    imagepng($image, $file); // сохраняем картинку
    imagedestroy($image);
    $res = base64_encode(file_get_contents($file)); // берем картинку в base64()
    unlink($file); // удаляем файл
    return $res;
}

public function check($tested) {
    $cap = get_cookie('cap');
    $r['error'] = '';
    if (!$cap) { // проверка жизни кука
        $r['error'] = 'Срок действия кода безопасности истек.';
    } elseif (strcmp($tested, $cap)) {
        $r['error'] = 'Коды безопасности не совпадают.';
    }
    delete_cookie('cap'); // удаляем кук в любом случае
    return $r;
}

Использование


В необходимых контроллерах готовим капчу следующим образом:

public function __construct() {
    ...
    $this->captcha = new \App\Libraries\Captcha();
    ...
}

public function some_function() {
    ...
    $data['captcha'] = $this->captcha->img_code( $this->captcha->generate_code() );
    return view('page/template', $data);
}

public function recaptcha(){ // для ajax-запроса
    return $this->captcha->img_code( $this->captcha->generate_code() );
}

В шаблоне создаем токен и кнопку для повторной генерации картинки:

...
<input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>" id="csrf"/>
...
<img src="data:image/png;base64,<?= $captcha ?>" id="cap" width="220" height="120"/>
<div id="ref" onclick="recaptcha()">⥁</div>
...

Дописываем роут для ajax запроса.

$routes->post('/recap', 'AuthController::recaptcha');

И сам ajax.

var numlog = 0;
function recaptcha() {
    if(numlog <= 5){
        $.ajax({
            type: 'post',
            url: '/recap',
            data: {csrf_token: $('#csrf').val()},
            success: function (result) {
                numlog++;
                $('#cap').attr('src', "data:image/png;base64," + result + '');
            }
        });
    }else{
        $('#ref').css('display', 'none');
    }
}

Итог


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

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



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

Полный код можно посмотреть на гитхабе. Буду рад услышать рекомендации и замечания.

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

    +2
    Верхний ряд из 4х капч мною не распознан, а я не робот :(
      +3
      а я не робот
      Я бы на вашем месте теперь не был так уверен.
        0
        Здравствуйте!
        Возможно тут дело в том, что в процессе отладки я решил очень много капч. Очень.
        Привык к шрифтам, цветам и мозг сам додумывает до приемлемого то, что многим не ясно сразу.

        Больше «человечности» можно достичь настройками (уменьшить шумы, число линий). Вот пример:

        0

        А цветовые решения учитывают особенности дальтонизма? Или дальтоники сразу мимо?

          +1

          Дело в том, что такой задачи не стояло, так как использую решение для себя.
          Однако, диапазон цветов настраивается. Можно увеличить контраст между текстом и фоном под определённые нужды.
          К сожалению, рекомендаций по палитре для дальтоников не видел, и не задумался над вопросом при создании капчи. Теперь задумаюсь, спасибо!

          +1
          Что только люди не придумают, чтобы не использовать reCAPTCHA… В ней не только защита от ботов, но и польза в оцифровке текстов. Человеко-ориентированная, а не вот это вот всё… Я тоже из картинок в посте одну или две распознал, об остальные сломал глаза. В реале я скорее плюнул бы на такой сайт, чем пытался распознать.
          Вы подумали о защите сайта (условно), но не подумали о пользователе. Это не лучшая стратегия, если вы делаете сайт для людей. Надеюсь, вы никогда не выкатите это в продакшн. А как тренировка навыков, конечно, проект годный!
            0
            Повзволю себе не согласиться. С ReCaptcha все не так однозначно.

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

            2. При использовании ReCaptcha становится зависим от работы google. Это все равно какой-никакой runtime vendor lock in. Конечно, google надежен, кто спорит, но чего только в нашей жизни не случается.

            В иготе отказались в пользу картиночной кириллической капчи. У пользователей стало меньше проблем, а значит и нам легче.
              0
              Отчасти соглашусь с вами. reCAPTCHA, конечно, не идеальна. Но она работает, и работает неплохо. Бывает, приходится немного повозиться с ней, но это максимум две-три попытки. Кроме того, в ней есть варианты для инвалидов, это важно (хотя у нас об этом как-то не принято думать).
              2. Однозначно соглашусь, что лучше не зависеть, чем зависеть. И если зависеть, то хотя бы от наших. Интересно, почему какой-нибудь ABBYY не выкатил аналог… Их профиль, мощностей у них более чем достаточно, монетизация вполне очевидна…
              Из альтернатив меня, как пользователя, радует вариант на форуме 4pda. Легко распознать, вероятно, не слишком элементарно ломается… Есть возможность не вводить.
              p.s. Вы использовали reCAPTCHA или No-CAPTCHA?
              0
              reCaptcha нельзя ставить на сайты с чувствительной информацией(банки, госуслуги, хранилища)
                0
                Я в госсекторе давно не работаю, подзабыл, что там и как… Но если бы я увидел подобную капчу в банке, не поленился бы накатать гневное письмо. Впрочем, за редким исключением что банковские, что гос сайты — это мрак и печаль в плане UX. При том, что у них есть элементарная возможность вообще не использовать капчу (у тинькова вроде не припомню), отличить бота от человека элементарно с помощью двухфакторной аутентификации…
                  0
                  Разумеется для разных задач разные решения.
                  Мне тоже не нравится искать светофоры, когда я хочу быстро получить информацию. Более. И смс лично мне не удобно получать, ибо нахожусь 50% суток в полете.

                  Но снова уточню, что есть способ облегчить задачу пользователю, поправив некоторые параметры (пример в комментарии выше).
                0

                Егор, что вы! Разумеется в данной конфигурации не прошу пользователей вводить это. Использую на закрытой админке сайта, где аккаунт только у админа.
                Более. Рекапчу "со светофорами" я перестал решать. Иногда погрузка и логика такая не очевидная, что вызывает больше раздражения, чем описанные варианты.


                Кроме того, решение гугл, безусловно хорошее, все же вынуждает обращаться к гуглу. Если есть интерес и возможность заменить внешние решения, люблю это делать.


                Спасибо за комментарий!

                  0
                  Для админки — отлично! Как заметили выше, возможность не зависеть от внешних поставщиков — это явный плюс!
                +1
                Я тоже как-то писал Капчу, может кому будет интересно — github.com/RollingTL/outline-captcha

                image

                image

                Как и у всех капч — проблема — иногда не поймешь, но лечится настройками. Часть букв выезжает за край картинки — что приводит к сложному ребусу. Но это можно убрать. Дальше можно было, но руки не дошли, кусочки букв вырезать и смещать, чтобы линии не были сплошными.

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

                  Спасибо! Нравится идея со смещением кусочков букв, надо попробовать.


                  Про непокорность ИИ скорее не соглашусь. Считаю преимуществом таких локальных решений с разной комбинацией настроек — именно локальность. Если есть появится уязвимость в рекапче от гугла, например, под угрозой будет много ресурсов. А поиск уязвимостей в собственных решениях иногда может потребовать от злоумышленников больше сил и времени, чем предполагаемая выгода.
                  На своём личном сайте вообще отказался от капчи при регистрации и авторизации. Предлагаю ввести в поле 10 на 10 букву, распознаваемую простенький нейросетью в личном кабинете, сразу после входа. Если за месяц пользователь этого не сделал беспощадная команда по крону его отправляет в архив ботов.

                  0
                  которая поможет мне уменьшить число запросов к серверу при авторизации в моем сайте с codeigniter 4.

                  Авторизация это процесс предоставление прав на выполнение действий.
                  Тут действительно верный термин использован?

                    0
                    Оговорился, спасибо!
                    Конечно имел ввиду аутентификацию, поскольку запрос к БД не происходит, пока капча не введена корректно.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое