Pull to refresh

Создание оригинальной каптчи, используя нейронные сети. Часть 1

Reading time 9 min
Views 2.8K
Как и у всех программистов, недавно у меня возникло желание изобрести собственный “велосипед”. Так как изобретать свою CMS, Framework, и т.д. уже не актуально, то мой взор обратился на каптчу. Казалось бы, что тут можно придумать оригинального, каких только каптч не существует: 2D-картинка, 3D-картинка, звуковая каптча, выбор “правильной” картинки. Но мне пришла в голову мысль, что создатели каптч думают как-то однобоко, то есть все хотят получить однозначно правильный ответ от пользователя (и обычно в тестовом поле), причем в простом виде, серверу лишь остается сравнить ответ с исходными данными! Вот я и решил исправить это дело и создать собственную “умную” каптчу.


Вступление

Идея моя проста: выводить пользователю в виде каптчи простые фигуры, например квадрат, круг, треугольник, и просить пользователя нарисовать эти самые фигуры. Конечно, 3 фигуры маловато, но зато можно их комбинировать и выводить в любом количестве. А на сервере принимать ответ пользователя (и как вы уже догадываетесь), распознавать данные образы с помощью нейронной сети (вот поэтому я пока и остановился на 3 фигурах, чтобы облегчить нейронную сеть, использовав только 3 выхода).
Недолго думая приступил я к реализации этой идеи, этот процесс я и собираюсь описывать в статьях...

Реализация

Начнем реализацию с клиентской части.
Отдавая дань моде, я воспользуюсь javascript фреймворком jQuery, да и признаюсь, он довольно таки сильно облегчит нашу задачу. В целях ещё большего облегчения задачи будет использоваться , как контейнер для рисования.
Раз я уже решил использовать jQuery, то ради красоты кода и удобства оформим всё в виде плагина, заодно и новичкам такой опыт пригодится. В завершенном виде выглядеть будет где-то так:
<!DOCTYPE html>
<html>
<head>
<title>Demo NNCaptcha</title>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript" src="nnCaptcha.js"></script>
</head>
<body>
<div id="nnCaptcha"></div>
<script type="text/javascript">
$(document).ready(function(){
   //Выводим каптчу
    $("#nnCaptcha").nnCaptcha();
});
</script>
</body>
</html>

Начнем с пустой “болванки” для плагина.
(function($) {
    // наши публичные методы
   var methods = {
       // инициализация плагина
       init:function(params) {
           var options = $.extend({}, defaults, params);

}
   };
    $.fn.nnCaptcha = function(method){
   
       if (this.length != 1) {
           $.error('not 1 element!');
           return;
       }
       // немного магии
       if ( methods[method] ) {
           // если запрашиваемый метод существует, мы его вызываем
           // все параметры, кроме имени метода прийдут в метод
           // this так же перекочует в метод
           return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
       } else if ( typeof method === 'object' || ! method ) {
           // если первым параметром идет объект, либо совсем пусто
           // выполняем инициализацию капчи
           return methods.init.apply( this, arguments );
       } else {
           // если ничего не получилось
           $.error( 'Метод "' +  method + '" не найден' );
       }
   };

})(jQuery);


Вот типичная “болванка” плагина для jquery, которая позволяет вызывать внутренние методы и передавать им параметры, например:
$("#nnCaptcha").nnCaptcha(‘reset’,1); //вызываеться метод reset с параметром 1 (очистка капчи от “рисулек”)

Займемся нашим главным методом init. В нем построим таблицу с N колонок, и двумя строками, вверху будем отображать картинки, а внизу пользователь будет их срисовывать. Повесим все необходимые обработчики и поместим все в DOM документа.

Начнем с построения таблицы. Легким путем я не пошел (вставить готовый html код), зная на что способна функция $(), я построил таблицу “на лету”. Получился довольно не тривиальный способ построения таблицы с переменным количеством столбцов:
    //Вставляем в контейнер капчи заготовку table
           this.append('<table class="tbCaptcha"><tbody></tbody></table>');
           //Строим таблицу из 2 строк и countCanvas колонок
           this.find("table.tbCaptcha").find('tbody')
               .append(function($){
                           var tr = $('<tr>');
                           for (i=0;i<countCanvas;i++)
                               tr.append($('<td>')
                                               .append($('<img>')
                                                   .attr('src', 'nnCaptcha.php?image=get')
                                       )
                               )
                           return tr;
                       }($)
               ).append(function($){
                           var tr = $('<tr>');
                           for (i=0;i<countCanvas;i++)
                               tr.append($('<td>')
                                       .append($('<canvas>')
                                           .attr('class', 'captcha')
                                       )
                                   )
                           return tr;
                       }($)
               );

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

Теперь самое интересное: реализовать возможность рисовать в canvas.
Главное — получить объект контекста canvas:
var ctx = canvas.getContext("2d");

А дальше для рисования линий нам понадобятся только эти методы:
beginPath(); //Для рисования линии этот метод должен быть вызван первым
moveTo(x, y); //устанавливает точку от которой начнется линия
lineTo(x, y); //устанавливаем точку к которой нужно провести линию
stroke(); //отрисовка линии

Осталось лишь правильно засунуть их в события mousedown, mouseup, mousemove, что мы и делаем, попутно выполняя ещё кое-какие действия:
           // Работаем только с элементами canvas
           this.find("canvas.captcha").each(function(i) {
               
               this.width = width;
               this.height = height;
               var ctx = this.getContext("2d");
               var elem = this;
               elCanvas[i] = elem;
               var drawing = false;
               pixCanvas[i] = createArrayPix();
               //биндим событие в своем пространстве имен
               $(this).bind("mousedown.nnCaptcha",function(e){
            //Получаем правильное координаты внутри canvas
                  var offset = $(elem).offset();
                  var x = e.pageX - offset.left;
                  var y = e.pageY - offset.top;
            //Заносим признак в бинарную матрицу этого canvas                               pixCanvas[i][x][y] = 1;
                  ctx.beginPath();
                  ctx.strokeStyle = options.selectedColor;
                   ctx.lineWidth = options.selectedWidth;
                   ctx.moveTo(x, y);
                   drawing = true;
                   elem.style.cursor = 'crosshair';
               });
               
               $(this).bind("mouseup.nnCaptcha",function(e){
                   
                   if (drawing)
                   {
                       var offset = $(elem).offset();
                       var x = e.pageX - offset.left;
                       var y = e.pageY - offset.top;
                       ctx.lineTo(x, y);
                       drawing = false;
                       pixCanvas[i][x][y] = 1;
                   }
                   
               });
               
               $(this).bind("mousemove.nnCaptcha",function(e){
                   
                   if (drawing)
                   {
                       var offset = $(elem).offset();
                       var x = e.pageX - offset.left;
                       var y = e.pageY - offset.top;
                       ctx.lineTo(x, y);
                       ctx.stroke();
                       ctx.moveTo(x, y);
                       pixCanvas[i][x][y] = 1;
                   }
                   elem.style.cursor = 'crosshair';
               });
               
           });

Вот теперь метод init нашего плагина готов, чтобы все заработало, добавим в плагин глобальные переменные и функцию:
    // значение по умолчанию
   var defaults = { selectedColor:'#000000',selectedWidth:1 };
   var countCanvas = 3;
   var elCanvas = [];
   var width = 100;
   var height = 100;
   var pixCanvas = [];
   
   function createArrayPix(){
       var a=new Array (width);
       for (i = 0; i < width; i++)
       {
           a[i]=new Array (height);
           for (j = 0; j < height; j++)
           {
               a[i][j] = 0;
           }
       }
       return a;
   }

Объясню зачем эти доп. действия, чтобы когда пользователь будет рисовать в канвазе, все координаты по которым рисовалось(ходила мышь), были занесены в бинарную матрицу — pixCanvas[Ncanvas][x][y] (“1” — координата “рисованая”, “0” — нет)
Конечно, куда проще было сделать сохранение картинки в канвазе в PNG c помощью метода:
canvas.toDataURL("image/png");

Он возвращает картинку канваза в base64, в виде data:URL, который можно было отправить на сервер и раскодировать, но получается при этом PNG картинку на сервере придеться ещё обрабатывать, чтобы получить входные данные на нашу нейронную сеть.
Проще на стороне клиента сразу создавать бинарную матрицу и примерно таким способом отправлять её на сервер:
ajax:function(n) {
           $.post("test.php", { matrix: JSON.stringify(pixCanvas[n]) } );
       }

Хотя конечно, можно обойтись и без ajax — создавать скрытый input на форме и json строку помещать там.
Вот теперь собираем весь код плагина вместе:
(function($) {

   // значение по умолчанию
   var defaults = { selectedColor:'#000000',selectedWidth:1 };
   var countCanvas = 3;
   var elCanvas = [];
   var width = 100;
   var height = 100;
   var pixCanvas = [];
   
   function createArrayPix(){
       var a=new Array (width);
       for (i = 0; i < width; i++)
       {
           a[i]=new Array (height);
           for (j = 0; j < height; j++)
           {
               a[i][j] = 0;
           }
       }
       return a;
   }
   
   // наши публичные методы
   var methods = {
       // инициализация плагина
       init:function(params) {
           // актуальные настройки, будут индивидуальными при каждом запуске
           var options = $.extend({}, defaults, params);
           //Вставляем в контейнер капчи заготовку table
           this.append('<table class="tbCaptcha"><tbody></tbody></table>');
           //Строим таблицу из 2 строк и countCanvas колонок
           this.find("table.tbCaptcha").find('tbody')
               .append(function($){
                           var tr = $('<tr>');
                           for (i=0;i<countCanvas;i++)
                               tr.append($('<td>')
                                       .append($('<img>')
                                           .attr('src', 'nnCaptcha.php?image=get')
                                       )
                               )
                           return tr;
                       }($)
               ).append(function($){
                           var tr = $('<tr>');
                           for (i=0;i<countCanvas;i++)
                               tr.append($('<td>')
                                       .append($('<canvas>')
                                           .attr('class', 'captcha')
                                       )
                                   )
                           return tr;
                       }($)
               );
           // Работаем только с элементами canvas
           this.find("canvas.captcha").each(function(i) {
               
               this.width = width;
               this.height = height;
               var ctx = this.getContext("2d");
               var elem = this;
               elCanvas[i] = elem;
               var drawing = false;
               
               pixCanvas[i] = createArrayPix();
               
               $(this).bind("mousedown.nnCaptcha",function(e){
                   var offset = $(elem).offset();
                   var x = e.pageX - offset.left;
                   var y = e.pageY - offset.top;
                   pixCanvas[i][x][y] = 1;
                   ctx.beginPath();
                   ctx.strokeStyle = options.selectedColor;
                   ctx.lineWidth = options.selectedWidth;
                   ctx.moveTo(x, y);
                   drawing = true;
                   elem.style.cursor = 'crosshair';
               });
               
               $(this).bind("mouseup.nnCaptcha",function(e){
                   
                   if (drawing)
                   {
                       var offset = $(elem).offset();
                       var x = e.pageX - offset.left;
                       var y = e.pageY - offset.top;
                       ctx.lineTo(x, y);
                       drawing = false;
                       pixCanvas[i][x][y] = 1;
                   }
                   
               });
               
               $(this).bind("mousemove.nnCaptcha",function(e){
                   
                   if (drawing)
                   {
                       var offset = $(elem).offset();
                       var x = e.pageX - offset.left;
                       var y = e.pageY - offset.top;
                       ctx.lineTo(x, y);
                       ctx.stroke();
                       ctx.moveTo(x, y);
                       pixCanvas[i][x][y] = 1;
                   }
                   elem.style.cursor = 'crosshair';
               });
               
           });
           
   
           return this;
       },
       
       test:function(n,x,y) {
           alert(pixCanvas[n][x][y]);
       },
       ajax:function(n) {
           $.post("test.php", { name: JSON.stringify(pixCanvas[n]) } );
       },
       
       // сброс
       reset:function(n) {
           //alert(n);
           if (n !== undefined){
               var c = elCanvas[n];
               var ctx = c.getContext("2d");
               ctx.clearRect(0, 0, c.width, c.height);
               pixCanvas[n] = createArrayPix();
           } else {
               $.each(elCanvas,function(i) {
                   var ctx = this.getContext("2d");
                   ctx.clearRect(0, 0, this.width, this.height);
                   pixCanvas[i] = createArrayPix();
               });
           }
       }
   };
   
   $.fn.nnCaptcha = function(method){
   
       if (this.length != 1) {
           $.error('not 1 element!');
           return;
       }
       // немного магии
       if ( methods[method] ) {
           // если запрашиваемый метод существует, мы его вызываем
           // все параметры, кроме имени метода прийдут в метод
           // this так же перекочует в метод
           return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
       } else if ( typeof method === 'object' || ! method ) {
           // если первым параметром идет объект, либо совсем пусто
           // выполняем метод init
           return methods.init.apply( this, arguments );
       } else {
           // если ничего не получилось
           $.error( 'Метод "' +  method + '" не найден' );
       }
   };

})(jQuery);

Конечно многое ещё “сырое”, и я постоянно все допиливаю, но идея думаю понятна. В живую работу плагина можно посмотреть тут. Ещё один «недостаток» этого кода, что он работает лишь в актуальных и современных браузерах (например, из-за того же canvas или «JSON.stringify()»), хотя при желании можно легко переделать всё под IE8- подобные браузеры…

Обобщение

Как показала дальнейшая практика, выбор квадрата 100х100 дает кол-во входов у нейронной сети — 10000, что довольно таки много. Скорее всего, квадраты для каптчи будут поменьше (да хотя бы 50х50), все будет зависеть от скорости обработки на сервере...

В качестве реализации нейронной сети в гугле я нашел «ANN — Artificial Neural Network for PHP 5.x».
Поначалу было интересно использовать “живой” пример реализации на PHP 5.3 с использованием неймспейсов, но когда я пытался обучить сеть с 10ю входами и 3мя выходами, это занимало больше 10 мин, представляю, сколько будет обучаться реальная выборка. В общем возможно придется даже тут изобретать свой велосипед. Можно конечно взять всем известную библиотеку FANN, но во первых она вряд ли дружит с PHP 5.3, во вторых на Windows откомпилированных решений нет (да и особо не нужно), а на хостинге хостер не дает поставить, но своего VDS пока нет. Да и если распространять каптчу, FANN придется занести в требования.
Хотя не обязательно привязываться к PHP на сервере, благо других серверных языков хватает...
Но вернемся к нашей каптче, понятное дело, что в таком виде она легко ломается ботами — достаточно отправить на сервер 3 “правильных” бинарных матриц (я уже не упоминаю про распознавание фигур, так как задача довольно проста). Хотя конечно на сервере можно исключить из результатов вероятности выше 0,95 (вряд ли пользователи рисуют как Пикассо и правильно нарисовать мышью может только бот), но всё же для надежности, я думаю, надо криптовать матрицу функцией с ключем, которую будет выдавать сервер (и не обязательно одной и той же), естественно в не читаемом и упакованном виде.
Теперь, если даже бот распознает правильно картинку — отправить на сервер правильную бинарную матрицу не сможет!
Дальше можно придумать ещё много чего:
  1. Искажение фигур которые надо нарисовать;
  2. Разные цвета в разных блоках;
  3. Защита по сессиям от перебора;
  4. Вложенные фигуры (а нарисовать одну надо);
и другие возможности.
Конечно уже то, что ответ формируется джаваскриптом, уже хоть какая защита от примитивных спам-ботов. Пока кто-то целенаправленно не займется взломом именно этой каптчи, вряд ли какой-то спам бот обойдет её, особенно если предусмотреть при взломе замену/перебор крипто-алгоритма (каких можно придумывать великое множество).

Заключение

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

P.S. Картинки на каптче нарисованы в Paint'e просто для примера! Алгоритм генерации ещё не придуман.
Tags:
Hubs:
+18
Comments 73
Comments Comments 73

Articles