О создание капчи на 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» иногда трудно разборчивы и путаються, их лучше исключить из алфавита
— расшифровать можно любую капчу, главное найти компромисс между читаемостью и сложностью обхода