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

в 13:54, , рубрики: nginx, метки:

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

  • требуется прегенерять достаточно большое количество «капч» и следить что бы они не заканчивались;
  • требуется выводить «капчу» рандомно при каждой отрисовки страницы (либо реализовать функционал обновления «капчи», если пользователь не смог разобрать на ней текст);
  • требуется где-то хранить знания о том, что написано на каждой из «капч» (объяснять почему эти знания не стоит запихивать в имя файла или 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.

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

Автор: phoinixrw

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js