Генерация уникального идентификатора пользователя средствами Nginx

Приветствую Вас, хабрачитатели!

Расскажу об одной задачке, которая встала передо мной, и как я ее решил.

Сразу оговорюсь — часовой поиск в G и в Я удовлетворяющего результата не принес, но за следующий час было реализовано собственное решение.

Все это пока не более чем эксперимент — есть белые пятна как в идеи, так и в реализации, на данном этапе нужно понять жить или не жить.


Суть задачи сводилась к тому, что мне требовалось уникально идентифицировать посетителя в независимости от природы и вероисповедания компонентов системы (Web-проект). Причем сделать это максимально просто, быстро и без большого оверхеда по быстродействию.
Важно заметить, что авторизация пользователя по логину/паролю или еще как не производится.

В качестве веб-сервера и первичного балансировщика нагрузки у меня имеется Nginx.

В моей системе для php используетcя php-fpm через fastcgi, так же через fastcgi работает c++ сервер бизнес логики.



Вот примерная схема:
image

К проблеме глобальной идентификации я пришел после того, как в систему был добавлен с++ сервер, который ловит некоторые запросы на себя.

Так как запрос к плюсовому серверу может быть раньше запроса к php-бэкенду, я так и не смог придумать/найти удобную, быструю, а самое главное красивую реализацию идентификации пользователя по PHPSESSID.

Реализовывать все это я решил средствами Nginx, а точнее его perl'овым модулем.
Это пока что единственный существенный минус решения — данный модуль не вкомпилен по умолчанию, то есть приходится пересобирать nginx:

$ ./configure --with-http_perl_module

Теперь задача сводиться к нескольким этапам:

Этап 1. Написать перловый модуль, генерирующий и проверяющий наличие идентификатора

1. Поверить на наличие установленнго идентификатора, например, в кукисах.
Идентификатор можно передавать не только в кукисах, в моем случая он именно там.

2а. Проверить на валидность идентификатор
Идентификатор проверяется на валидность, т.е. он проверяется быль ли он генерирован модулем или вкралась ошибка.
Это требуется для дальнейшей работы моих компонентов.
Так же есть некоторые идеи как этот идентификатор использовать для балансировки нагрузки на стороне клиента.

Если идентификатор валиден, завершаем процедуру генерации.
Если нет генерируем новый (п. 2б) — возможно тут следует как-то по другому реагировать, надо подумать.

2б. Сгенировать идентификатор
Тут идет генерация идентификатора с использованием случайно последовательности + некоторые данные от пользователя.
Сейчас идентификатор состоит из 32 байт (hexstr) случайной последовательности и 32 байт (hexstr) дайджеста (см. ниже).
Конечно это можно и будет уменьшено.

package session;
use strict;
use Digest::MD5 qw(md5_hex);
my $secret_key = '__TOP_SECRET__KEEP_IT_IN_BANK__'; #секретный ключ, нужен для генерации идентификатора
my $cookie_name = 'SID'; # название куки где храним идентификатор
my $rand_len = 16; # длина случайной последовательности

my $hex_length = $rand_len * 2;  
my $hex_mask = "H".$hex_length;
my $digest_length = 32; # длина дайнжеста в hexstr - 32 байта.

# процедура генерации идентификатора
sub hash
{
    # data - случайная последовательность, ng - nginx объект.
    my ($data,$ng) = @_;
    # в генерации участвую юзерагент и ip пользователя
    return md5_hex($data."_".$secret_key."_".$ng->header_in("User-Agent")."_".$ng->remote_addr);
}

# отсюда читаем случайные данные. по MANу, вроде, /dev/random можно верить.
# дескриптор будет открыт один раз и держатся все время работы nginx.
# закрывается автоматом при завершении работы.
open(my $rand, '<', "/dev/random");

sub gen
{
    # ng - nginx объект
    my $ng = shift;
    # проверка на наличие идентификатора в куках
    # первые 32 байт (hexstr) случайная последовательность
    # остальные 32 байта (hexstr) дайджест (см. sub hash)
    if ($ng->header_in("Cookie")=~/$cookie_name=(\w{$hex_length})(\w{$digest_length});?/) {
        if ($2 eq hash($1, $ng)) {
            return "$1$2";
        }
    }
    
    # читаем случайную строку
    read($rand, my $data, $rand_len);
    
    # переводим ее в hexstr
    my $h = unpack($hex_mask, $data);
    
    # склеиваем ее с дайджестом (см. sub hash)
    my $id = $h.hash($h, $ng);
    
    # устанавливаем куку
    $ng->header_out("Set-Cookie","$cookie_name=$id;");
    # возвращаем идентификатор nginxу
    return $id;
}

1;
__END__


Этап 2. Подружить компоненты системы с идентификатором

Для того чтобы подключить данный модуль правим nginx.conf
    http {
        ...
        perl_modules conf/perl; # директория, где хранится наш модуль
        perl_require session.pm; # файл модуля
        perl_set $sid session::gen; # переменная, в которую будет сохраняться идентификатор
        ...
 
        server { 
            ..
            location ~*\.php$ {
                root           html/www;
                fastcgi_pass   http://backend_upstreams;
                fastcgi_index  index.php;
                fastcgi_param  SCRIPT_FILENAME  $document_root/$fastcgi_script_name;
                include        fastcgi_params;
                fastcgi_param  SID $sid; # передача идентфикатора по FastCGI в бэкенд
            }
            
            location ~*\.tst$ {            
                fastcgi_pass   unix:/tmp/cpp_server;
                include        fastcgi_params;
                fastcgi_param  SID $sid; # передача идентификатора по FastCGI в бэкенд
            }
        }
        ...
    }


Этап 3. Проверка, что мы не завалим все нафиг

Был написан небольшой нагрузочный тест. Я использовал стандратный перловый Benchmark.
#!/usr/bin/perl
use strict;
use Benchmark;
use Digest::MD5 qw(md5_hex);

my $secret_key = '__TOP_SECRET__KEEP_IT_IN_BANK__';
my $cookie_name = 'SID';
my $rand_length = 16;
my $hex_mask = "H".($rand_length * 2);
open(my $rand, '<', "/dev/random");

sub hash
{
    my ($data) = @_;
    my $hash = md5_hex($data."_".$secret_key."_Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7_127.0.0.1");
    return $hash;
}

sub gen
{
    read($rand, my $data, $rand_length);
    my $h = unpack($hex_mask,$data);
    my $id = $h.hash($h);
    my $ng = "$cookie_name=$id;";
    return $ng;
}

my $t0 = new Benchmark;
for (my $i =0; $i < 1000000;++$i) {
    gen();
}
my $t1 = new Benchmark;
my $td = timediff($t1, $t0);
print "Total:".timestr($td)."\n";


Результат работы на 600Mhz VDSке:
Total: 6 wallclock secs ( 5.75 usr + 0.30 sys = 6.05 CPU)
Т.е. на генерацию одного идентификатора уходит ~6 * 10-6 сек.
Предположим, самый худший вариант проверка + генерация на запрос = 12 * 10-6 сек.
Остальное пока я тестировать не стал, но там конечно есть где потюнить.

PHP


Доступ из php-бэкенда к идентификатору — $_SERVER['SID'];
Так же можно установить данный идентификатор как session_id
<?
    session_id($_SERVER['SID']);
    session_name('SID'); //иначе в куках будут два поля с одинаковыми значениями PHPSESSID, SID
    session_start();
?>


В этом случае, если сессию хранить например в БД или Memcache, то все компоненты системы будут иметь доступ к данным сессии (правда, вижу проблему по поводу локирования записи сессии).

UPD:

Почему не ngx_http_userid_module


Спасибо Demetros за правильные вопросы.

Есть две очень существенные причины почему данный модуль не подходит(подробнее ):
  1. Нет контроля валидности идентификатора пользователя
  2. При первом запросе нет возможности передать uid в backend

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 33

    +2
    А зачем читать случайные данные из /dev/random? Здесь же не нужен криптографический надежный источник случайных чисел. А так у чтения из /dev/random есть два минуса:
    1. Если в системе недостаточно энтропии, чтение из /dev/random может заблокировать вызывающий поток. Думаю, nginx будет не рад :)
    2. Лишние файловые операции. Зачем, если можно обойтись?

    Ведь здесь хватит и простого random(). Если мучит паранойя — можно захешировать.
      +1
      Я много где спараноил)))
      Думаю использовать /dev/urandom — псевдослучайные, видимо к этому придется прийти.

      Я читал ман и видел описания функции, так что когда я делал бенчмарк, то специально проверил — ~50Mb в секунду выдает /dev/random, и затыков я не обнаружил.
      Хотя может еще конечно и наткнусь.

      Если использовать перловый rand(), то время работы увеличивается в 6 раз — только что проверил.
        +1
        Ну у меня на декстопе /proc/sys/kernel/random/ показывает 180, а /proc/sys/kernel/random/ (сколько бит энтропии надо для разблокировки вызывающего процесса) — 64. Так что значения довольно таки стрёмные.
        На сервере количество энтропии может быть ещё меньше — мышки и клавы то нету…

        Так что в общем из /dev/random лучше не читать без сильной на то обходимости.
          0
          В FreeBSD видимо своя магия /dev/random.
          Думаю на сервере основной генератор энтропии все же сетевуха.

          ну я думаю из urandom достаточно будет
      +1
      А почему не хранить php-сессии в memcached? Вы всегда сможете произвольно управлять данными в сессии из приложения на C++ (в т.ч. создавать новые сессии), и в то же время всё это будет нативно работать в php бэкэндах.
        +1
        Что касается блокировок при доступе к сессиям в memcached, используйте не pecl-memcached, а pecl-memcache. Там есть и блокировки, и нормальная возможность работы с нескольими memcached-серверами, и проч.
          0
          вот так я и не хотел делать, я завязываюсь на определенную архитектуру,
          и получается генерация идентификатора (в вашем случае сессии) будет в двух местах.
          +1
          Возможно спрошу глупость, но почему не ngx_http_userid_module?
            0
            Данный модуль немного по другому работает, он ставит куку уже после отработки бэкенда.
            Т.е. он возвращает ее броузеру, а бэкэнд в этот момент ничего не знает о uid. Т.е. первый запрос в бэкэнд будет ошибочен, что не допустимо.

            SID присутствует, а uid нет… честно честно модуль userid включен)

            <?
            print_r($GLOBALS);
            ?>
            


            Array
            (
                [GLOBALS] => Array
             *RECURSION*
                [_POST] => Array
                    (
                    )
            
                [_GET] => Array
                    (
                    )
            
                [_COOKIE] => Array
                    (
                    )
            
                [_FILES] => Array
                    (
                    )
            
                [_SERVER] => Array
                    (
                        [PHP_FCGI_MAX_REQUESTS] => 1000
                        [PHP_FCGI_CHILDREN] => 5
                        [USER] => www
                        [PATH] => /sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin
                        [FCGI_ROLE] => RESPONDER
                        [SCRIPT_FILENAME] => /usr/local/nginx/html/www//index.php
                        [QUERY_STRING] => 
                        [REQUEST_METHOD] => GET
                        [CONTENT_TYPE] => 
                        [CONTENT_LENGTH] => 
                        [SCRIPT_NAME] => /index.php
                        [REQUEST_URI] => /index.php
                        [DOCUMENT_URI] => /index.php
                        [DOCUMENT_ROOT] => /usr/local/nginx/html/www
                        [SERVER_PROTOCOL] => HTTP/1.1
                        [GATEWAY_INTERFACE] => CGI/1.1
                        [SERVER_SOFTWARE] => nginx/1.1.12
                        [REMOTE_ADDR] => 127.0.0.1
                        [REMOTE_PORT] => 60816
                        [SERVER_ADDR] => 127.0.0.1
                        [SERVER_PORT] => 80
                        [SERVER_NAME] => localhost
                        [REDIRECT_STATUS] => 200
                        [SID] => adb95450d71c9cbd5f1ecc97343256e19401c78b586585c3e969cc882a12744c
                        [HTTP_HOST] => 62.109.16.240
                        [HTTP_USER_AGENT] => Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.23) Gecko/20110920 Firefox/3.6.23 s024vp
                        [HTTP_ACCEPT] => text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
                        [HTTP_ACCEPT_LANGUAGE] => ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3
                        [HTTP_ACCEPT_ENCODING] => gzip,deflate
                        [HTTP_ACCEPT_CHARSET] => windows-1251,utf-8;q=0.7,*;q=0.7
                        [HTTP_KEEP_ALIVE] => 115
                        [HTTP_CONNECTION] => keep-alive
                        [PHP_SELF] => /index.php
                        [REQUEST_TIME] => 1325859793
                    )
            
                [_ENV] => Array
                    (
                    )
            
                [_REQUEST] => Array
                    (
                    )
            
            )
            


              0
              fastcgi_param UID $uid_set;

              так тоже не работает?
                0
                не работает, передает пустой параметр в backend

                я бился с этим модулем какое-то время, так и не получилось заставить его делать что мне надо. к великому моему сожалению.
                  0
                  Да, странный модуль, работает как фильтр даже после SSI-модуля.
                  Похоже, с его помощью можно только логгировать посетителей.
                    0
                    Надо Сысоеву написать, чтоб переделал, либо тут какая-то скрытая магия))
            0
            >Реализовывать все это я решил средствами Nginx, а точнее его perl'овым модулем.
            Это пока что единственный существенный минус решения — данный модуль не вкомпилен по умолчанию, то есть приходится пересобирать nginx:
            $ ./configure --with-http_perl_module

            Не соглашусь, не нужно руками перекомпиливать nginx, в FreeBSD вы просто ставите галочку что бы собирался с этим модулем, в ubuntu просто ставите пакет nginx-extra с данным модулем, насчет других систем точно не скажу, но думаю тоже есть уже готовые пакеты со всеми сущ. модулями.
              +1
              я просто из сорцов последнюю версию собирал
              +1
              Напишите теперь это в виде модуля к NGINX на C, это проще чем может показаться )
              Кстати для nginx есть такой модуль, set-misc-nginx-module, он расширяет набор команд модуля rewrite различными условиями, присваиваниями и функциями. Например можно присвоить переменной случайное число, взять md5 от текущего времени+случайного числа и использовать полученное как идентификатор пользователя. Кстати функцию генерации случайного числа — нужно было для одного проекта, решил не писать с нуля, а дополнить хороший модуль еще одной функцией…
                0
                я писал модули для nginx — веселое и интересное занятие)
                  0
                  и надо было и этот написать!
                  времени займет один-два дня — если есть опыт, если его нет — то приобретете что-то более ценное, чем потраченное время.
                    +1
                    я не думаю, что на каждую идею надо писать модуль и тем более я не думаю, что выигрыш по производительности будет колоссальный, я даже думаю он будет значительно меньше, чем кажется.

                    когда я еще начинал кодить, мне тоже казалось что все быстрые программки надо писать в виде LKM.

                    peace — V
                –2
                А что вы храните в сессии пользователя на сервере? Неужели это нельзя хранить у самого пользователя в cookie?
                  +1
                  А если ip юзера сменится?
                    0
                    в текущей реализации будет новый индетификатор, его можно убрать из процедуры генерации. в моей системе это критично.
                      0
                      А не хотите добавить еще HTTP_ACCEPT заголовок в юзер агенту для идентификации?
                        0
                        с каких полей брать дайджест зависит от требований системы, но не следует параноить еще сильнее меня
                      0
                      > Так как запрос к плюсовому серверу может быть раньше запроса к php-бэкенду, я так и не смог придумать/найти удобную, быструю, а самое главное красивую реализацию идентификации пользователя по PHPSESSID.

                      В этом случае плюсовый сервер может просто позвать php-бэкенд и спросить PHPSESSID, разве нет?
                        +1
                        нет.
                        смысл не в том, чтобы узнать PHPSESSID, а в том чтобы и php-бэкенд и плюсовый сервер идентифицировали пользователя идентичным идентификатором идентификации :)

                          0
                          Смысл я понял. Вы сначала хотели использовать в качестве такого идентификатора PHPSESSID, но споткнулись на процитированной проблеме и стали искать другие пути. Я предложил вам решение этой проблемы.
                            +1
                            Как я понял…
                            Плюсовый код, если у него не выставлена сессия должен
                            грузить, что то типа 127.0.0.1/start_session.php,
                            где ему будет возвращаться PHPSESSID,
                            и он его будет ставить в куки текущего соединения.

                            По мне так решение должно быть красивым и максимально быстрым.
                            Но тут ни того ни другого, но суть даже не в этом —
                            если я уйду вообще от php и такого понятия как PHPSESSID в моей системе больше не будет, в вашем случае мне все равно придется писать аналогичный код.

                            Но все равно спасибо и с праздником.
                              0
                              >если я уйду вообще от php и такого понятия как PHPSESSID
                              а что уже в планах?

                              вопросы:
                              1) а какую роль играет плюсовой сервер?
                              2) почему выбран FastCGI, SCGI проще в реализации и быстрее
                              3) какие сетевые либы используешь в плюсовом сервере, как построена сетевая часть?
                              4) возможность масштабирования твоей системы?
                                0
                                не то чтобы прям в планах, но есть такая тенденция, которая мне не нравится:
                                давайте напишем сайт на php, прикрутим nginx, php-fpm, eaccelator, hip-hop, memcache и т.д. и т.п.

                                я конечно люблю пых, но есть очень прикольная серия статей www.insight-it.ru/highload/ один из смыслов которой «мы использовали пых, потому что так сложились обстоятельства».

                                1) там такая штука большая и ИМХО прикольная, я потом отдельно статью напишу.
                                2) не вижу очень уж большой разницы. Если я не ошибаюсь SCGI бинарный, но нет нативной поддержки nginxа (есть модули вроде). Но я почитаю, обещаю.
                                3) libfcgi, boost::asio
                                4) на первом месте стоит производительность, на втором масштабируемость.

                                я фанат плюсов, было бы побольше времени в сутках написал бы все на плюсах)))

                                  0
                                  > 1) там такая штука большая и ИМХО прикольная, я потом отдельно статью напишу
                                  меня интересуют всякого рода подобные штучки, так как сам пишу их в онном количестве
                                  так что, поделись секретом тен-на-тет
                                  >2) не вижу очень уж большой разницы. Если я не ошибаюсь SCGI бинарный, но нет нативной поддержки nginxа (есть модули вроде).
                                  существует поддержка во всех современных WEB серверах: Апач, Энджи, Лайти
                                  я написал по этому случаю либу, очень дружит с nginx. Именно для таких всяких нестандартных штучек, правда в настоящий момент она усовершенствуется.
                                  > 4) на первом месте стоит производительность, на втором масштабируемость.
                                  Одно не далеко ушло от другого. Правда многое зависит от особенностей проекта, но потенциал масштабируемости должен быть заложен.

                                  > я конечно люблю пых, но есть очень прикольная серия статей www.insight-it.ru/highload/ один из смыслов которой «мы использовали пых, потому что так сложились обстоятельства».
                                  Я люблю пых уже более 10 лет, по этому на своей шкуре прекрасно знаю, что такое: выбрали именно эту технологию потому-что… её хорошо знает мой начальник, а потом разгребаем…
                        0
                        У себя на проекте в интересах кастомной системы статистики мы uid генерили пыхом и отдавали в заголовках, а nginx потом это в access_log писал. Но у нас конечно же не было плюсового бэкенда.
                          0
                          посмотрите в сторону nginx_http_userid_module, если я Вас правильно понял, он делает именно то что вам надо

                        Only users with full accounts can post comments. Log in, please.