Этой статьей мне хотелось бы открыть мою первую серию статей "Джентельменский набор PHP программиста". Как и во всей остальной серии здесь пойдет речь о программировании на PHP для интернет-проектов, но в каждой статье я буду выбирать один узкий аспект и на протяжении всей статьи буду стараться показать возможные варианты его реализации и применения.

Сегодня таким аспектом станет защита интернет-ресурса от возможного возникновения нежелательного контента со стороны пользователей с помощью технологии captcha (точнее о "графическом" варианте ее реализации), о которой уже неоднократно шла речь.

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

Подготовка

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

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

  1. Описание класса, генерирующего изображение
  2. Файл, который будет вызываться browser'ом. В нем будет подключено описание нашего класса, выбор настроек данного конкретного изображения и выполнено создание объекта класса, в соответствии с выбранными настройками.

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

Итак, создание объекта будем производить максимально простым способом, параметрами укажем белый и черный цвета. Заготовка для самого класса будет выглядеть примерно следующим образом (предположим, что он хранится в файле captcha.class.php):

<?php
class Captcha
{
   private $string; // генерируемый текст
   private $bgcol;  // основной цвет фона
   private $fgcol;  // основной цвет текста
   private $height; // высота изображения
   private $width;  // ширина изображения
   function __construct($bgcol,$fgcol)  // конструктор, вызывается при создании экземпляра класса
   {
   }
}
?>

Задаем параметры

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

<?php
private function generateImage()  // генерация изображения
{
  $this->width=250;
  $this->height=80;
  $this->fgcol=$fgcol;
  $this->bgcol=$bgcol;
  $this->generateSymbols();
 }
 private function generateSymbols()   // генерация четырех цифр
 {
    $this->string=$this->leadingZero(rand()%10000,4);
 }
 private function leadingZero($num,$length) // дополнения числа num лидирующими нулями
 {                        // до длины length
  $str=strrev($num);
  for($i=strlen($str);$i<$length;++$i)$str.="0";
  return strrev($str);
 }
}
?>

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

Генерируем изображение

Если забыть, что текст необходимо искажать, то функция, генерирующая изображение выглядела бы просто как:

<?php
private function generateImage()  // генерация изображения
{
   $im=@imagecreatetruecolor($this->width,$this->height);
   $bcol=imagecolorallocate($im,$this->bgcol[0],$this->bgcol[1],$this->bgcol[2]);
   $fcol=imagecolorallocate($im,$this->fgcol[0],$this->fgcol[1],$this->fgcol[2]);
   imagefill($im,0,0,$bcol);
   imagettftext($im,40,10,20,25,$fcol,"./font/font_name.ttf",$this->string));
   header('Content-Type: image/png');
   imagepng($im);
   imagedestroy($im);
}
?>
В данном методе используются функции модуля PHP под названием GD, основывающегося на одноименной библиотеке, убедитесь, что на Вашем хостинге этот модуль установлен.

Реально же ей пользоваться не стоит - такое изображение с легкостью поддается OCR. Полученный текст необходимо тем или иным образом исказить. Для вывода изображения используется формат PNG, но никто не мешает воспользоваться JPEG или GIF, для этого достаточно заменить везде png на название соответствующего формата.

Искажаем текст

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

  • использование нестандартных шрифтов - функция imagettftext позволяет использовать произвольный шрифт в формате Truetype, чем и необходимо воспользоваться. В Сети можно найти огромное количество бесплатных шрифтов в этом формате. По возможности стоит выбирать шрифты, максимально не похожие на любой стандартный, но при этом легко читающиеся.
  • использование нескольких шрифтов - сделав подборку подходящих шрифтов, можно не останавливаться на каком-то одном, а сделать выбор текущего шрифта случайным из списка.
  • случайный выбор цветов - усложняет работу OCR и в большинстве случаев не сильно мешает восприятию человеком.
  • случайное расположение символов - еще один способ усложнить работу программам, пытающимся прочитать текст.
  • неравномерный фон - изобразив на фоне какой-либо абстрактный набор любых фигур, можно заставить программу-посетителя подумать что какая-то часть из них является символом. Например, пересечение двух прямых линий часто распознается как буква T или L. Неплохим вариантом является написание на фоне других символов другим цветом, сильно отличающимся от основного и близким к цвету фона.

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

<?php
private function generateImage() // генерация изображения
{
   $im=@imagecreatetruecolor($this->width,$this->height);  // создаем пустое изображение
   $mcol=imagecolorallocate($im,$this->fgcol[0]+rand()%100+80,$this->fgcol[1]+rand()%30+150,$this->fgcol[2]-rand()%55); // выбираем случайным образом
   $kcol=imagecolorallocate($im,$this->fgcol[0]+rand()%100+80,$this->fgcol[1]+rand()%30+150,$this->fgcol[2]-rand()%20); // несколько цветов
   $lcol=imagecolorallocate($im,$this->bgcol[0]-rand()%20,$this->bgcol[1]-rand()%20,$this->bgcol[2]-rand()%20);
   $bcol=imagecolorallocate($im,$this->bgcol[0],$this->bgcol[1],$this->bgcol[2]);
   $fcol=imagecolorallocate($im,$this->fgcol[0],$this->fgcol[1],$this->fgcol[2]);
   imagefill($im,0,0,$bcol);  // заполняем изображение фоном
   $array=array(6,7,6,6,20,20,25,26,31,32,37,39,41); // список названий подходящих шрифтов
   $n=$array[rand()%count($array)];  // наугад выбираем из них один
   $m=rand()%50+1;
   $k=rand()%50+1;
   for($i=0;$i<$m;++$i)
   imageline($im,0,rand()%$this->height,$this->width,rand()%$this->height,$lcol); // создаем на фоне несколько линий
   for($i=0;$i<$k;++$i)
   imageline($im,rand()%$this->width,0,rand()%$this->width,$this->height,$lcol); // и еще несколько
   /*
   Генерируем текст: две строки на фон, а также интересующие нас символы по одному.
   */
   imagettftext($im,rand()%20+40,rand()%100-50,rand()%$this->height*0.8,rand()%50+25,$kcol,"./font/".$k.".ttf",$this->randomString(rand()%15));
   imagettftext($im,rand()%40+35,rand()%70-35,rand()%$this->height*0.8,rand()%25+25,$mcol,"./font/".$m.".ttf",$this->randomString(5+rand()%4));
   for($i=0;$istring);++$i)
   imagettftext($im,rand()%10+33,rand()%70-35,15+$i*$this->width/5*1.1+rand()%5,rand()%7+$this->height*0.73,$fcol,"./font/".$n.".ttf",$this->string[$i]);
   for($i=0;$i<$m/10;++$i)
   imageline($im,0,rand()%$this->height,$this->width,rand()%$this->height,$mcol); // еще линии
   for($i=0;$i<$k/4;++$i)
   imageline($im,rand()%$this->width,0,rand()%$this->width,$this->height,$mcol);  // и еще немного
   for($i=0;$i<$k/6;++$i)
   imageline($im,rand()%$this->width,0,rand()%$this->width,$this->height,$fcol);  // и еще чуть-чуть
   header('Content-Type: image/png');
   imagepng($im);
   imagedestroy($im);
}
private function randomString($length)  // генерируем случайный набор символов заданной длины
{
  $list="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVXYZ!@#$%^&**()-=_+.,<>/\|;:";
  for($i=0,$str="";$i<$length;++$i)$str.=substr($list,mt_rand(0,strlen($list)-1),1);
  return $str;
}
?>

Стоит заметить, что конкретные цифры необходимо подбирать индивидуально, в примере они указаны абсолютно произвольно. Использование конкретно этих же цифр приведет к далеко не самым лучшим результатам.

Сборка

Не стоит забывать, что помимо генерации самого изображения, необходимо передать написанный текст другому скрипту, который будет сверять данные. Удобнее всего это делать через глобальный массив $_SESSION.

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

<?php
class Captcha
{
   private $string; // генерируемый текст
   private $bgcol;  // основной цвет фона
   private $fgcol;  // основной цвет текста
   private $height; // высота изображения
   private $width;  // ширина изображения
   function __construct($bgcol,$fgcol)  // конструктор, вызывается при создании экземпляра класса
   {
      $this->width=250;
      $this->height=80;
      $this->fgcol=$fgcol;
      $this->bgcol=$bgcol;
      $this->generateSymbols();
      $this->generateImage();
   }
   private function generateImage() // генерация изображения
   {
      $im=@imagecreatetruecolor($this->width,$this->height);  // создаем пустое изображение
      $mcol=imagecolorallocate($im,$this->fgcol[0]+rand()%100+80,$this->fgcol[1]+rand()%30+150,$this->fgcol[2]-rand()%55); // выбираем случайным образом
      $kcol=imagecolorallocate($im,$this->fgcol[0]+rand()%100+80,$this->fgcol[1]+rand()%30+150,$this->fgcol[2]-rand()%20); // несколько цветов
      $lcol=imagecolorallocate($im,$this->bgcol[0]-rand()%20,$this->bgcol[1]-rand()%20,$this->bgcol[2]-rand()%20);
      $bcol=imagecolorallocate($im,$this->bgcol[0],$this->bgcol[1],$this->bgcol[2]);
      $fcol=imagecolorallocate($im,$this->fgcol[0],$this->fgcol[1],$this->fgcol[2]);
      imagefill($im,0,0,$bcol);  // заполняем изображение фоном
      $array=array(6,7,6,6,20,20,25,26,31,32,37,39,41); // список названий подходящих шрифтов
      $n=$array[rand()%count($array)];  // наугад выбираем из них один
      $m=rand()%50+1;
      $k=rand()%50+1;
      for($i=0;$i<$m;++$i)
      imageline($im,0,rand()%$this->height,$this->width,rand()%$this->height,$lcol); // создаем на фоне несколько линий
      for($i=0;$i<$k;++$i)
      imageline($im,rand()%$this->width,0,rand()%$this->width,$this->height,$lcol); // и еще несколько
      /*
      Генерируем текст: две строки на фон, а также интересующие нас символы по одному.
      */
      imagettftext($im,rand()%20+40,rand()%100-50,rand()%$this->height*0.8,rand()%50+25,$kcol,"./font/".$k.".ttf",$this->randomString(rand()%15));
      imagettftext($im,rand()%40+35,rand()%70-35,rand()%$this->height*0.8,rand()%25+25,$mcol,"./font/".$m.".ttf",$this->randomString(5+rand()%4));
      for($i=0;$istring);++$i)
      imagettftext($im,rand()%10+33,rand()%70-35,15+$i*$this->width/5*1.1+rand()%5,rand()%7+$this->height*0.73,$fcol,"./font/".$n.".ttf",$this->string[$i]);
      for($i=0;$i<$m/10;++$i)
      imageline($im,0,rand()%$this->height,$this->width,rand()%$this->height,$mcol); // еще линии
      for($i=0;$i<$k/4;++$i)
      imageline($im,rand()%$this->width,0,rand()%$this->width,$this->height,$mcol);  // и еще немного
      for($i=0;$i<$k/6;++$i)
      imageline($im,rand()%$this->width,0,rand()%$this->width,$this->height,$fcol);  // и еще чуть-чуть
      header('Content-Type: image/png');
      imagepng($im);
      imagedestroy($im);
   }
   private function generateSymbols()   // генерация четырех цифр
   {
      $this->string=$this->leadingZero(rand()%10000,4);
   }
   private function leadingZero($num,$length) // дополнения числа num лидирующими нулями
   {                        // до длины length
      $str=strrev($num);
      for($i=strlen($str);$i<$length;++$i)$str.="0";
      return strrev($str);
   }
   private function randomString($length)  // генерируем случайный набор символов заданной длины
   {
      $list="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVXYZ!@#$%^&**()-=_+.,<>/\|;:";
      for($i=0,$str="";$i<$length;++$i)$str.=substr($list,mt_rand(0,strlen($list)-1),1);
      return $str;
   }
}
?>

Слегка доработав его и приведя в более подходящий вид, можно добиться генерации изображений, выглядящих например вот так:

CAPTCHA Sample

Специально не выкладываю уже доведенный до ума класс, чтобы у читателей не возникало желания просто взять и воспользоваться им, это приведет лишь к очередной серии captcha-клонов.

13 января 2008 |  Иван Блинков  |  PHP