Быстрая «капча»

от автора

Одним из способов защиты от «роботов» и «ботов» является установка «капчи» на ключевые действия пользователей. Но красивую и сложную «капчу» генерить довольно ресурсоемко и не всегда оправдано: пользователь может выйти на страницу с «капчей» и не вводить её, а ресурсы на её отрисовку уже потрачены. Что бы не тратить ресурсы на генерацию «капчи» во время отрисовки формы, её можно создавать заранее и выводить как статичную картинку, но при этом мы сталкиваемся с определенными техническими трудностями, а именно:

  • требуется прегенерять достаточно большое количество «капч» и следить что бы они не заканчивались;
  • требуется выводить «капчу» рандомно при каждой отрисовки страницы (либо реализовать функционал обновления «капчи», если пользователь не смог разобрать на ней текст);
  • требуется где-то хранить знания о том, что написано на каждой из «капч» (объяснять почему эти знания не стоит запихивать в имя файла или cookies я не буду);
  • «капча» одноразовая, то есть, после того как она была показана и пользователь ввел правильный код, использовать повторно мы её не должны;

Задача

Есть некий файл, который доступен для скачивания, но при этом требуется вводить капчу.

Решение 1 — простое

Алгоритм

image

Структура папок
  • /spool/projects/capcha/ — root директория проекта;
index.html

Все просто, форма в которую мы через SSI вставляем индексный файл из папки /capches/. Индексный файл у нас будет рандомный с использованием модуля ngx_http_random_index_module.

<html> <body>     <form action="/download" method="GET">         <!--#include virtual="/capches/"-->         <input type="text" name="code" value="">         <input type="submit">     </form> </body> </html> 
Файлы в папке capches

В файле такая часть формы:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAA8AgMAAADQw5Y7AAAACVBMVEX///8AAAAAyMjozb6ZAAAB 50lEQVQ4jYWVS27DIBCGx0hUhLWVNa1aifYUTU5AJFuqukp3PQbNKgfIAVhGPmXnAW5SWR4jJ5hv GP4ZXjCtPnACBxFfA+Az0HMFCNjg8X2CM9apRG6nB608WlPxjDMULEY6A1fxIxMuQBV8qT8AmXjg DmxQ6IcK1sBPE46AxjIgEoaOtGUwaEFv4QY0YOyqeHGPrkW2m7FIr9pKFS7YsfRZW1XGulzDhroW wVwLDUeKi0Ik39OUyXtEjCF5xKGpQ6dvGSZgmeJTsGfz2ALznM4ouEovrBiAIzC18TTj3LTx/x0O om6OO4iB4BoZa6MpiTC3nHg5BJM9J5YDk2oxvBwE18R6VtYS2nBTR9pkHfCn4ACzuszKSmto2MtX jZvrUfBZ3LCNI+yEOLE/V2Eynr/+6Qo3uHSD6x/HwSaT3IiR3uNsbRyyufSXLm12R/iPt90X4j49 22QhLeBjn31KEVK30NtaxJv3wwckM4YbXDjIzeG4zRaGnnp/cypoJlpvQ86RWVgc+4HwJ6SXZWxt 6H/20PfL0rrhddztM4Y1+jtpkjVJ6tXLbP7P2krO12ZMmW91tShrTV2pK+tc2SXqHlN36Nr+Vk4H 5WxRTiblXFNOReVMVU/ktfNcuQ2Uu0S5iZR7bP2S/AWn1wwm+CZwIQAAAABJRU5ErkJggg== "> <input type="hidden" name="md5" value="IODn35yg2gLtnSRhyKyK6g"> <input type="hidden" name="key" value="5e12c2002a0370826a9dee5f6a55f5e3"> 

Так как после успешной проверки капчи нам потребуется удалять файл, поэтому изображение мы вставляем в него как data:image/png;base64. key — собственно имя файла, md5 — это md5 (base64) сумма от: key + код на капче + соль.

Конфиг nginx
server {     listen                      80;     server_name                 capcha.local;      root                        /spool/projects/capcha/;     index                       index.shtml;      location / {         ssi                     on;     }     location /capches/ {         random_index            on;     }     location /download {         proxy_pass              %backend_uri%;     }     location /file {         deny all;     } } 

Здесь все просто:

  • для корневого location разрешаем SSI директивы;
  • для location /capches/ — устанавливаем рандомный индекс;
  • location /download — проксируем на бекенд;
  • для location /file — закрываем доступ, что бы по прямой ссылке невозможно было скачать файл.
Прегенерация частей форм

Как именно будет генерится капча и по какому алгоритму в рамках текущей статьи совершенно совершенно не важно. Я взял первый попавшийся модуль GD::SecurityImage и получился вот такой скрипт. Скрипт просто генерит картинку

#!/usr/bin/perl use uni::perl; use GD::SecurityImage; use Digest::MD5 qw|md5_base64 md5_hex|; use MIME::Base64; # Основные параметры: my $root_dir    = '/spool/projects/capcha'; my $salt        = 'salt'; my $img_limit   = 10; # Считаем сколько файлов форм my $counter = 0; $counter++ foreach <$root_dir/capches/*>; while ($counter < $img_limit) {     my ($image_data, $mime_type, $random_number) = GD::SecurityImage         ->new(width => 120, height => 60, gd_font => 'giant')             ->random                 ->create('normal', 'circle', '#000000', '#00c8c8')                     ->out;     my $filename = md5_hex(rand);     my $encoded_image = encode_base64($image_data);     my $md5 = md5_base64($filename.' '.$random_number.' '.$salt);     next if $md5 =~ /[\+\/]/; # нам надо base64url но к сожалению в Digest::MD5 такого метода нет, а доработать - лень     open (my $form_fh, '>', $root_dir.'/forms/'.$filename.'.html') or die $!;         print $form_fh '<img src="data:image/'.$mime_type.';base64,'.$encoded_image.'">             <input type="hidden" name="md5" value="'.$md5.'">             <input type="hidden" name="key" value="'.$filename.'">';     close $form_fh;     $counter++; }  1; 

Как видно, имя файла соответствует параметру key, что бы было достаточно просто определить какой файл удалять в последствии.

Отдача файла

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

Рассмотрим более необычное решение…

Решение 2 — pure nginx

Сразу хочу отметить, данное решение предлагается исключительно в ознакомительных целях. Если вы частично или совсем НЕ ПОНИМАЕТЕ механизма работы даже НЕ пытайтесь использовать это в продакшене. Те, кто полностью понимают механизм и сами поймут, надо ли им это.

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

Отдача файла осуществляется только при достижении определенных условий: введен правильный код и файл с частью формы в наличии и мы можем его удалить, и мы его удалили. Отдавать файлы — достаточно просто, это nginx умеет очень хорошо и нативно. Проверить md5 сумму — нет ничего проще, для этого есть модуль ngx_http_secure_link_module. Манипуляции с файлами можно осуществлять с помощью модуля ngx_http_dav_module, но тут немного сложнее, потому как нужно будет изменить метод GET на DELETE, а после удаления файла с частью формы еще требуется отдать запрашиваемый файл. Для этого дополнительно воспользуемся модулем ngx_http_proxy_module в связке с заголовком X-Accel-Redirect. Еще добавлю возможность скачивания только определенного списка файлов с помощью модуля ngx_http_map_module.

Добавим в форму запроса файла hidden поле file в котором укажем алиас файла для скачки:

<html> <body>     <form action="/download" method="GET">         <!--#include virtual="/capches/"-->         <input type="hidden" name="file" value="arch1">         <input type="text" name="code" value="">         <input type="submit">     </form> </body> </html> 

Для папки capches добавим символьную ссылку dav: ln -s /spool/projects/capcha/capches /spool/projects/capcha/dav

Конфиг nginx будет выглядеть так:

# Проставляем соответствия алиасов и реальных имен файлов map $file_alias $filename {     default                         'fail';     'arch1'                         'archive1.zip';     'arch2'                         'archive2.zip';     'arch3'                         'archive3.zip'; }  server {     listen                      80;     server_name                 captcha.local;      root                        /spool/projects/capcha/;     index                       index.shtml;      location / {         ssi                     on;     }     location /capches/ {         random_index            on;     }     location /download {         # Увы переданные аргументы мы можем получить только из строки запроса, поэтому обязательно GET         if ($request_method != 'GET') {             return              301         /fail;         }         # Проверяем что аргумент у нас передан без спец символов, так как относительно него мы будем удалять файл         if ($arg_key !~ '^\w+$') {             return              301         /fail;         }         # Проверяем md5 сумму         secure_link             $arg_md5;         secure_link_md5         "$arg_key $arg_code salt";         if ($secure_link = "") {             return              301         /fail;         }         if ($secure_link = "0") {             return              301         /fail;         }         # После определения $file_alias автоматически переопределяется $filename         set                     $file_alias $arg_file;         proxy_intercept_errors  on;         # Производим проксирование на location /dav с подменой метода на DELETE и URI на имя удаляемого файла части формы         proxy_pass              http://127.0.0.1/dav/$arg_key.html;         proxy_method            DELETE;         proxy_set_header        Host        $host;         # Так же определяем при проксировании дополнительный заголовок в котором укажем файл, которые потом потребуется отдать пользователю         proxy_set_header        X-File      $filename;     }     location /dav/ {         # Собственно символьная ссылка dav -> capches сделана именно для этого location, впрочем можно сделать и rewrite, на любителя         # Разрешаем доступ только с IP сервера, что бы данный location не был доступен извне, но доступен для локального проксирования         allow                   127.0.0.1; # я тестировал локально поэтому такой IP         deny                    all;         # Можно только удалять         dav_methods             DELETE;         # В случае правильного выполнения запроса обратно отдаем внутренний редирект на location /file/ с именем файла         add_header              X-Accel-Redirect        "/file/$http_x_file";         # так же это имя указываем дополнительно в заголовке, что бы файл скачивался с правильным именем         add_header              Content-Disposition     "attachment; filename=\"$http_x_file\"";         # иначе редиректим на страницу ошибки         error_page              403 404                 =301 /fail;     }     location /file {         # location внутренний и доступен только по внутреннему редиректу         internal;         # Пытаемся прочитать файл или отдаем пустоту         try_files               $uri        =204;     }     location /fail {         return                  200         'FAIL CODE';     } } 

И да, более простое правильное и понятное решение — использовать модуль ngx_http_perl_module, и всю логику осуществлять на уровне Perl, но, как я сказал выше данное решение я предлагаю исключительно в ознакомительных целях, что бы развить гибкость мышления при использовании стандартных инструментов.
Заключение

В заключении можно сказать следующее: Да, можно сделать статичную капчу и производить её валидацию прямо на уровне nginx не затрагивая при этом backend, но есть определенные сложности при работе с подобным решением, а именно:

  • конкурентность — когда одновременно рандомно выберется один и тот же файл с частью формы двум разным пользователям, использовать её сможет только один, кто первый введет правильный код;
  • требуется постоянно прегенерять достаточное количество «капч»;
  • определенные трудности с масштабированием, впрочем, они решаемы;

Оригинал статьи находится здесь.

P.S.

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

ссылка на оригинал статьи http://habrahabr.ru/post/196654/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *