- PVSM.RU - https://www.pvsm.ru -
Одним из способов защиты от «роботов» и «ботов» является установка «капчи» на ключевые действия пользователей. Но красивую и сложную «капчу» генерить довольно ресурсоемко и не всегда оправдано: пользователь может выйти на страницу с «капчей» и не вводить её, а ресурсы на её отрисовку уже потрачены. Что бы не тратить ресурсы на генерацию «капчи» во время отрисовки формы, её можно создавать заранее и выводить как статичную картинку, но при этом мы сталкиваемся с определенными техническими трудностями, а именно:
Есть некий файл, который доступен для скачивания, но при этом требуется вводить капчу.
Все просто, форма в которую мы через 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>
В файле такая часть формы:
<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 + код на капче + соль.
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;
}
}
Здесь все просто:
Как именно будет генерится капча и по какому алгоритму в рамках текущей статьи совершенно совершенно не важно. Я взял первый попавшийся модуль 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, в случае правильного набора удаляем файл с частью формы, отдаем файл пользователю.
Рассмотрим более необычное решение…
Сразу хочу отметить, данное решение предлагается исключительно в ознакомительных целях. Если вы частично или совсем НЕ ПОНИМАЕТЕ механизма работы даже НЕ пытайтесь использовать это в продакшене. Те, кто полностью понимают механизм и сами поймут, надо ли им это.
Вообще-то основной целью этого решения является то, какие возможности предоставляют, те или иные казалось бы стандартные модули 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, но есть определенные сложности при работе с подобным решением, а именно:
Оригинал статьи находится здесь [2].
Как валидировать таким образом формы (например: регистрации), я, если честно, не думал, но постараюсь подумать об этом ближайшее время.
Автор: phoinixrw
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/nginx/45132
Ссылки в тексте:
[1] мышления: http://www.braintools.ru
[2] здесь: http://doc.prototypes.ru/server/nginx/ngx_http_random_index_module/capcha/
[3] Источник: http://habrahabr.ru/post/196654/
Нажмите здесь для печати.