- требуется прегенерять достаточно большое количество «капч» и следить что бы они не заканчивались;
- требуется выводить «капчу» рандомно при каждой отрисовки страницы (либо реализовать функционал обновления «капчи», если пользователь не смог разобрать на ней текст);
- требуется где-то хранить знания о том, что написано на каждой из «капч» (объяснять почему эти знания не стоит запихивать в имя файла или cookies я не буду);
- «капча» одноразовая, то есть, после того как она была показана и пользователь ввел правильный код, использовать повторно мы её не должны;
Задача
Есть некий файл, который доступен для скачивания, но при этом требуется вводить капчу.
Решение 1 — простое
Алгоритм
Структура папок
- /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/
Добавить комментарий