PHP

Нестандартная CAPTCHA

О создание капчи на PHP+Jquery без задействования графических изображений.

Прелюдия

Сегодня Интернет просто изобилует бесконечным количеством инструкций, в которых различные авторы, рассказывают о своём собственном способе организации защиты от ботов, жаль только, что большинство этих авторов на языке носят типун. Вот, к примеру, плагин jQuery под названием Real Person, он создаёт на странице, нечто подобное:

Причём, стоит обратить внимание на то, что все буквы созданы при помощи всего лишь одного символа — астериска, без использования каких либо изображений. На сайте автора есть примеры, которые показывают, как легко можно изменить длину и набор символов для генерации защитного кода. Также там вы найдёте пример серверного скрипта для проверки правильности ввода символов:
    if (rpHash($_POST['realPerson']) == $_POST['realPersonHash']) {

Ё-моё, и вот он Epic Fail! Невиданная досада, автор предлагает нам полностью доверять данным, пришедшим от пользователя. Такая проверка надёжна… он хоть верит в это сам?! С учётом того, что исходный код функции «rpHash()», также описан на сайте автора этого плагина, то с чистой совестью можно было писать и такую проверку:
    if ($_POST['In'])== $_POST['Out']) {

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

Метод решения

Данная CAPTCHA представляет собой, набор букв латинского алфавита и арабских цифр. Каждый символ представляет собой матрицу размером 7х7. Любая ячейка матрицы может быть занята или свободна. Занятая ячейка, по умолчанию, имеет тёмный фон, а свободная – прозрачный.

Пример буквы «M».

Любую ячейку можно однозначно определить с помощью двух координат – x и y. Чтобы в целостности воссоздать символ достаточно знать координаты только одного вида ячеек. Разумно хранить координаты занятых ячеек, так как их количество в несколько раз меньше свободных. Для хранения этой информации используется массив:
Array(x1, y1, x2, y2…,xN, yN)
Исходя из вышеизложенного, вот так выглядит буква «M»:
    $abc['m'] = array(1,1,7,1,1,2,2,2,6,2,7,2,1,3,3,3,5,3,7,3,1,4,4,4,7,4,1,5,7,5,1,6,7,6,1,7,7,7);

При генерации строки символов, надо учитывать смещение координаты x, каждого последующего символа, на n*7 ячеек, где n – количество символов–предшественников:


Соответственно, данную строку можно инициализировать как массив, получившийся в результате слияния двух массивов m(x, y) и a(x+7, y).
Сам генератор массива координат строк выглядит так:
        // весь доступный алфавит
        $alphanum = 'abcdefghijkmnopqrstuvxyz0123456789';
        // цикл, генерации символов,
        // количество итераций цикла равно количеству символов в строке
        for ($i = 0; $i < $the_number_of_letters; ++$i) {
         // случайно выбирается символ
         $letter = $alphanum[intval(mt_rand(0, 33))];
         // создаётся массив символов $array_str
         foreach ($abc[$letter] as $key=>$val)
          // расставляются «правильные» координаты в массиве
          array_push($array_str, ($key%2 == 0)?$val+($i*7):$val);
          // запоминается сама строка
          $di_captcha_str .= $letter;
         }

Визуализируются символы, при помощи тега с заданным выравниванием по левому краю — float:left и чётко заданными размерами длины и высоты. В принципе, для данной цели подойдёт любой блочный элемент, но является одним из самых коротких доступных тегов, поэтому выбор пал на него. Сам код вывода:
JavaScript (jQuery)
        // длина блока с тегами
        // от этого значения зависит количество тегов в каждой строке
        // вычисляется по формуле количество_ячеек+размер_отступа_между_ячейками*7
        // +двойной_размер_ячейки – отступ между символами
        // и всё это умножается на количество символов n
        $('#DICaptchaPic').css('width', ((((cell_size+2)*6)+(3*cell_size)+1)*n));
        // переменная для хранения списка тегов 
        var html_p_tag = '';
        // цикл обхода всего массива с ячейками
        for (i = 1; i <= 7*7*n; ++i) { 
         // если ячейка кратна семи, значит она крайняя в символе 
         // и поэтому после неё необходим отступ межсимвольный
         var style = (i%7 == 0)?'margin-right: '+2*cell_size+'px;':'';
         // если ячейка занята, то её фон чёрного цвета
         for (j = 0; j < data[1].length; j += 2) style +=(((i%(data[0]*7)==0)?(data[0]*7):i%(data[0]*7)) == data[1][j] && Math.ceil(i/(data[0]*7)) == data[1][j+1])?'background-color: #000;':'';
         // закрывается тег
         html_p_tag += '<p'+((style=='')?'':' style=\''+style+'\'')+'>'; }

Примечание. Сложность данного алгоритма оценивается как O(n*n), из-за вложенного цикла. Его можно усовершенствовать, добавив конструкцию break во второй цикл, которая будет вызываться при успешном выполнение условия, тогда во втором цикле будет просматриваться только часть массива, а не весь массив, как сейчас. Так же можно вынести внутренний цикл наружу, а в первом присваивать все тегам уникальный id, по которому второй цикл их легко распознает. Хотя это и приведёт к небольшому увеличению кода, но скорость работы заметно увеличиться.
В результате получаются вот такие строки:

Критика и доработка

1. Достаточно перехватить массив с координатами при загрузке его js`ом и сравнить его с шаблонами, чтобы расшифровать строку. Если же перед тем как функция get() вернёт этот массив, в нём перемешать пары, то это усложнит в n раз использование шаблонов.
Поэтому, перед тем как вернуть массив координат, в классе вызывается метод shuffle2, который аккуратно перемешивает массив, не путая пары x и y.
PHP
        function shuffle2($array) {
         for ($i = 0; $i < count($array); $i += 2)
          for ($j = count($array)-2; $j > $i; $j -= 2)
           if (mt_rand(0, 1) > 0) {
            $array[$i]+=$array[$j]; $array[$j]=$array[$i]-$array[$j]; $array[$i]-=$array[$j];
            $array[$i+1]+=$array[$j+1]; $array[$j+1]=$array[$i+1]-$array[$j+1]; $array[$i+1]-=$array[$j+1];
           }
         return $array;
        }

2. Если добавить в массив координат случайный шум, то это практически сделает невозможным использование шаблонов.
Шум будет двух видов, шум который забивает занятые ячейки, тем самым делая их свободными и шум, распространяющийся на фон, который некоторые свободные ячейки преобразует в занятые.
Для этого был немного модифицирован генератор массива координат строк:
PHP
        // $this->noise инициализируется при создание объекта класса
        // может принимать значения от 0(нет шума) до 10
        $alphanum = 'abcdefghijkmnopqrstuvxyz0123456789';
        // основной цикл
        for ($i = 0; $i < $this->the_number_of_letters; ++$i) {
         $letter = $alphanum[intval(mt_rand(0, 33))];
         for ($j = 0; $j < count($this->abc[$letter]); $j += 2)
          // шум внутренний
          if (mt_rand(1, 100) > $this->noise*5)
           array_push($this->array_str, $this->abc[$letter][$j]+($i*7), $this->abc[$letter][$j+1]);
         // шум для фона
         for ($j = 0; $j < 7*7*($this->noise/20); ++$j) {
          array_push($this->array_str, mt_rand(1, 7)+($i*7), mt_rand(1, 7));
         }
         $_SESSION['di_captcha_str'] .= $letter;
        }
        return $this->shuffle2($this->array_str);

В итоге:

Полностью рабочий пример

Основной класс:
PHP
        // В конструкторе задаётся длина строки символов, по умолчанию 6.
        // методы класса:
        // shuffle2() – перемешивает массив.
        // set() – может менять значения некоторых полей класса.
        // get() – возвращает массив координат и заносит в сессию саму строку.
        // check() – принимает введённый пользователем текст и сравнивает его со строкой, записанной в сессии.
        namespace di;
        class captcha {
        private $str, $array_str = array(), $abc = array(), $the_number_of_letters = 6, $noise = 1;
        function __construct($the_number_of_letters = 6) {
        $this->the_number_of_letters = $the_number_of_letters;
        $this->abc['a'] = array(4,1,3,2,5,2,3,3,5,3,2,4,6,4,2,5,3,5,4,5,5,5,6,5,1,6,7,6,1,7,7,7);
        $this->abc['b'] = array(1,1,2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,1,4,2,4,3,4,4,4,5,4,6,4,1,5,7,5,1,6,7,6,1,7,2,7,3,7,4,7,5,7,6,7);
        $this->abc['c'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,1,4,1,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
        $this->abc['d'] = array(1,1,2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,1,4,7,4,1,5,7,5,1,6,7,6,1,7,2,7,3,7,4,7,5,7,6,7);
        $this->abc['e'] = array(1,1,2,1,3,1,4,1,5,1,6,1,7,1,1,2,1,3,1,4,2,4,3,4,4,4,1,5,1,6,1,7,2,7,3,7,4,7,5,7,6,7,7,7);
        $this->abc['f'] = array(1,1,2,1,3,1,4,1,5,1,6,1,7,1,1,2,1,3,1,4,2,4,3,4,4,4,1,5,1,6,1,7);
        $this->abc['g'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,1,4,1,5,5,5,6,5,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
        $this->abc['h'] = array(1,1,7,1,1,2,7,2,1,3,7,3,1,4,2,4,3,4,4,4,5,4,6,4,7,4,1,5,7,5,1,6,7,6,1,7,7,7);
        $this->abc['i'] = array(1,1,2,1,3,1,4,1,5,1,6,1,7,1,4,2,4,3,4,4,4,5,4,6,1,7,2,7,3,7,4,7,5,7,6,7,7,7);
        $this->abc['j'] = array(7,1,7,2,7,3,7,4,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
        $this->abc['k'] = array(1,1,7,1,1,2,5,2,6,2,1,3,3,3,4,3,1,4,2,4,1,5,3,5,4,5,1,6,5,6,6,6,1,7,7,7);
        $this->abc['l'] = array(1,1,1,2,1,3,1,4,1,5,1,6,1,7,2,7,3,7,4,7,5,7,6,7,7,7);
        $this->abc['m'] = array(1,1,7,1,1,2,2,2,6,2,7,2,1,3,3,3,5,3,7,3,1,4,4,4,7,4,1,5,7,5,1,6,7,6,1,7,7,7);
        $this->abc['n'] = array(1,1,7,1,1,2,2,2,7,2,1,3,3,3,7,3,1,4,4,4,7,4,1,5,5,5,7,5,1,6,6,6,7,6,1,7,7,7);
        $this->abc['o'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,1,4,7,4,1,5,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
        $this->abc['p'] = array(1,1,2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,1,4,2,4,3,4,4,4,5,4,6,4,1,5,1,6,1,7);
        $this->abc['q'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,1,4,7,4,1,5,5,5,7,5,1,6,6,6,2,7,3,7,4,7,5,7,7,7);
        $this->abc['r'] = array(1,1,2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,1,4,2,4,3,4,4,4,5,4,6,4,1,5,5,5,1,6,6,6,1,7,7,7);
        $this->abc['s'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,2,4,3,4,4,4,5,4,6,4,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
        $this->abc['t'] = array(1,1,2,1,3,1,4,1,5,1,6,1,7,1,4,2,4,3,4,4,4,5,4,6,4,7);
        $this->abc['u'] = array(1,1,7,1,1,2,7,2,1,3,7,3,1,4,7,4,1,5,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
        $this->abc['v'] = array(1,1,7,1,1,2,7,2,2,3,6,3,2,4,6,4,3,5,5,5,3,6,5,6,4,7);
        $this->abc['w'] = array(1,1,7,1,1,2,7,2,1,3,7,3,1,4,4,4,7,4,1,5,3,5,5,5,7,5,1,6,2,6,6,6,7,6,1,7,7,7);
        $this->abc['x'] = array(1,1,7,1,2,2,6,2,3,3,5,3,4,4,3,5,5,5,2,6,6,6,1,7,7,7);
        $this->abc['y'] = array(1,1,7,1,2,2,6,2,3,3,5,3,4,4,4,5,4,6,4,7);
        $this->abc['z'] = array(1,1,2,1,3,1,4,1,5,1,6,1,7,1,6,2,5,3,4,4,3,5,2,6,1,7,2,7,3,7,4,7,5,7,6,7,7,7);
        $this->abc['0'] = array(3,1,4,1,5,1,2,2,6,2,1,3,5,3,7,3,1,4,4,4,7,4,1,5,3,5,7,5,2,6,6,6,3,7,4,7,5,7);
        $this->abc['1'] = array(4,1,3,2,4,2,2,3,4,3,4,4,4,5,4,6,1,7,2,7,3,7,4,7,5,7,6,7,7,7);
        $this->abc['2'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,7,3,6,4,4,5,5,5,2,6,3,6,1,7,2,7,3,7,4,7,5,7,6,7,7,7);
        $this->abc['3'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,7,3,5,4,6,4,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
        $this->abc['4'] = array(5,1,4,2,5,2,3,3,5,3,2,4,5,4,1,5,2,5,3,5,4,5,5,5,6,5,7,5,5,6,5,7);
        $this->abc['5'] = array(1,1,2,1,3,1,4,1,5,1,6,1,7,1,1,2,1,3,2,3,3,3,4,3,5,3,6,3,7,4,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
        $this->abc['6'] = array(3,1,4,1,5,1,6,1,2,2,1,3,1,4,2,4,3,4,4,4,5,4,6,4,1,5,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
        $this->abc['7'] = array(1,1,2,1,3,1,4,1,5,1,6,1,7,1,6,2,5,3,4,4,3,5,2,6,1,7);
        $this->abc['8'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,2,4,3,4,4,4,5,4,6,4,1,5,7,5,1,6,7,6,2,7,3,7,4,7,5,7,6,7);
        $this->abc['9'] = array(2,1,3,1,4,1,5,1,6,1,1,2,7,2,1,3,7,3,2,4,3,4,4,4,5,4,6,4,7,4,7,5,6,6,2,7,3,7,4,7,5,7);
        }
         
        private function shuffle2($array) {
        for ($i = 0; $i < count($array); $i += 2)
         for ($j = count($array)-2; $j > $i; $j -= 2)
          if (mt_rand(0, 1) > 0) {
          $array[$i]+=$array[$j]; $array[$j]=$array[$i]-$array[$j]; $array[$i]-=$array[$j];
          $array[$i+1]+=$array[$j+1]; $array[$j+1]=$array[$i+1]-$array[$j+1]; $array[$i+1]-=$array[$j+1];
          }
        return $array;
        }
         
        function set($name, $val) {
        switch ($name) {
         case 'the_number_of_letters':
          $this->the_number_of_letters = (int)$val;
          break;
         case 'noise':
          $this->noise = (int)$val;
          break;
         default:
          return false;
        }
        return true;
        }
         
        function get() {
        $alphanum = 'abcdefghijkmnopqrstuvxyz0123456789';
        unset($_SESSION['di_captcha_str']);
        for ($i = 0; $i < $this->the_number_of_letters; ++$i) {
         $letter = $alphanum[intval(mt_rand(0, 33))];
         //foreach ($this->abc[$letter] as $key=>$val)
         // array_push($this->array_str, ($key%2 == 0)?$val+($i*7):$val);
         for ($j = 0; $j < count($this->abc[$letter]); $j += 2)
          if (mt_rand(1, 100) > $this->noise*5)
          array_push($this->array_str, $this->abc[$letter][$j]+($i*7), $this->abc[$letter][$j+1]);
         for ($j = 0; $j < 7*7*($this->noise/20); ++$j) {
          array_push($this->array_str, mt_rand(1, 7)+($i*7), mt_rand(1, 7));
         }
         $_SESSION['di_captcha_str'] .= $letter;
        }
        return $this->shuffle2($this->array_str);
        }
         
        function check($in_string) {
        echo $in_string.'|'.$_SESSION['di_captcha_str'];
        return (strtolower($in_string) == $_SESSION['di_captcha_str'])?true:false;
        }
        }

После создания phar и сжатия его, получается файл di_captcha.class.phar.gz весом 3.53кб.
Прример использования класса:
PHP+html
    // index.php
    // Прример использования класса
    session_start();
    // THE_NUMBER_OF_LETTERS – константа, количество символов в строке
    define('THE_NUMBER_OF_LETTERS', 6);
    // При запросе от js о генерации строки, возвращается количество символов и сам массив координат.
    if (isset($_POST['action']{14}) && $_POST['action'] == 'captcha_refresh') {
     require 'phar://di_captcha.class.phar.gz/di_captcha.class.php';
     $captcha = new di\captcha();
     $captcha->set('noise', 0);
     echo json_encode(array(THE_NUMBER_OF_LETTERS, $captcha->get()));
    } else {

    ?>
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset='utf-8'>
    <title>Test</title>
    <link rel='stylesheet' media='all' href='style.css'>
    <script type='text/javascript' src='jquery-1.6.1.min.js' charset='utf-8'></script>
    src='script.js' charset='utf-8'></script>
    </head>
    <body>
    <p id='Title'>Сим-сим, откройся!</p>
    <p id='Msg'>
    <?php
     if (isset($_POST['action']{11}) &#&&; $_POST['action'] == 'captcha_send') {;
      require 'phar://di_captcha.class.phar.gz/di_captcha.class.php';
      $captcha = new di\captcha();
      echo ($captcha->check($_POST['text_captcha']))?'Сим-сим открылся!':'К сожалению, Вы ошиблись...';
     }
    ?>
    </p>
    <form action='index.php' method='post'>
     <div id='DICaptchaPic'></div>
     <p style='padding: 0 10px;'>
      <input type='text' name='text_captcha' id='text_captcha' value='<?php echo $_POST['text_captcha']; ?>' placeholder='6 символов с картинки'><br><label for='text_captcha'>*aнти-спам</label> <ahref='#' onclick='di_captcha_refresh(); return false;'>Не вижу</a>
     </p>
     <p style='padding: 10px 0;'>
      <input type='hidden' name='action' value='captcha_send' />
      <input type='submit' name='submit' value='Проверить' />
     </p>
    </form>
    </body>
    </html>
    <?php
    }

При запросе от js о генерации строки, возвращается количество символов и сам массив координат.
И, собственно, js скрипт:
JS
        /* script.js */
        /* cell_size – размер ячеек в пикселях */
        var cell_size = 3;
        function di_captcha_refresh() {
        $.post('./index.php', {action: 'captcha_refresh'},
         function(data) {
          var data = eval(data);
          $('#DICaptchaPic').css('width', ((((cell_size+2)*6)+(3*cell_size)+1)*data[0]));
          var html_p_tag = '';
          for (i = 1; i <= 7*7*data[0]; ++i) {
          var style = (i%7 == 0)?'margin-right: '+2*cell_size+'px;':'';
          for (j = 0; j < data[1].length; j += 2) style +=(((i%(data[0]*7)==0)?(data[0]*7):i%(data[0]*7)) == data[1][j] && Math.ceil(i/(data[0]*7)) == data[1][j+1])?'background-color: #000;':'';
          html_p_tag += '<p'+((style=='')?'':' style=\''+style+'\'')+'>'

      }  $('#DICaptchaPic').html(html_p_tag); } )} $(document).ready(function() { $('#DICaptchaPic').css('overflow', 'hidden'); $('#DICaptchaPic').css('height', (cell_size+2)*7); di_captcha_refresh(); $('#DICaptchaPic').click(function() {di_captcha_refresh();}); $('#text_captcha').focus()})* This source code was highlighted with Source Code Highlighter.

P.S.

1. Если в класс генерации строк добавить шаблоны символов, по несколько шаблонов на символ, то это ещё больше усложнит расшифровку посредствам шаблонов.
2. Работники _http://decaptcher.com, _http://captchabot.com и _http://antigate.com/ скажут вам спасибо за такую капчу.
3. Эта статья была опубликована моим другом, в разделе веб разработка несколько дней назад, набрала 16 минусов, 14 плюсов и 11 коментов. Потом была похоронена модераторами по причине «не умеет читать правила сайта (публикует чужие посты и клянчит инвайты)», статья написана специально для сайта, и инвайты ни кто не клянчил, а уточнили, что он не автор статьи. Потом попала в песочницу и заслужила инвайт.
4. Краткий смысл 11 комментариев:
— чтобы обойти капчу надо перехватить post запрос и расшифровать текст по шаблонам
— с шумом при расшифровки будет большой брак
— с шумом буквы «О» и «D», «C» и «G» иногда трудно разборчивы и путаються, их лучше исключить из алфавита
— расшифровать можно любую капчу, главное найти компромисс между читаемостью и сложностью обхода
Добавлено: 17 Августа 2013 09:13:38 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...