Pull to refresh

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

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

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

Сразу оговорюсь — часовой поиск в 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
Tags:
Hubs:
+34
Comments33

Articles