Капча для 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');     } }

Итог

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

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

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

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

ссылка на оригинал статьи https://habr.com/ru/post/493752/