воскресенье, 20 февраля 2011 г.

Как устроены сессии в Mojolicious?!

В этом посте я не буду рассказывать про API для работы с сессиями, это можно найти в документации к Mojolicious, а постараюсь объяснить как устроены сессии внутри.

Mojolicious использует сookies для хранения сессий. И это достаточно важный момент. Такой подход позволяет нам следовать REST идеологии. Мы можем отказаться от лишних состояний на стороне сервера, нам не нужно обеспечивать общего пространства для доступа к сессиям с разных серверов.

Но имеется несколько важных нюансов:
  1. Старайтесь не делать сессии большими, поскольку эти данные будут передаваться при каждом запросе, а максимально безопасный размер cookie - 4кб.
  2. Вопрос авторства данных.
С первым пунктом все ясно, а относительно второго, то это решается либо шифрованием либо подписью cookies. Mojolicious использует подписанные(signed) cookies.

Что должна обеспечивать подпись?
Начнем с того, что любая подпись (включая подпись ручкой на бумаге) должна обеспечить следующие две вещи:
  1. Идентифицировать автора подписи.
  2. Гарантировать, что после того, как документ подписали, он не изменялся.
Подписанный документ не подразумевает того, что он должен быть зашифрован. Соответственно подписанные cookies не обязаны быть зашифрованными.
Мы оставляем документ в открытом виде и лишь добавляем к нему подпись(цифровую или ручкой на бумаге)


Стандартная реализация
Допустим, нам необходимо подписать текст "foobar".
Подпись создаем следующим образом: md5_hex("foobar" . $secret_key). Если $secret_key равен  "mysecret", то мы получим подпись вида - "c232332be32fec146ef0fdb82bb913d2". Подписанный документ может выглядеть так -  "foobar---c232332be32fec146ef0fdb82bb913d2".  И если кто-то попробует изменить текст с "foobar" на "barfoo", то он сможет это сделать, но не сможет переподписать измененный документ, поскольку он не знает $secret_key.


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

Таким образом, сессия сохраненная в cookies доступна для чтения на клиентской стороне ( при помощи javascript ), но недоступна для изменения.

Необходимо обратить внимание на следующие вещи:
1. Если кто-то узнал $secret_key, то его необходимо изменить, и это автоматически сделает все сессии недействительными.
2. Поскольку сессии не хранятся на сервере, то нельзя просто взять и удалить файл. И для того, чтобы сессия не была вечно действительна в нее добавляют временную метку, которая определяет время жизни сессии.

Как реализованы сессии в Mojolicious?
Вот как выглядит cookie с сессией в Mojolicious приложении -
"mojolicious=BAcIMTIzNDU2NzgECAgIAwMAAAAKATUHAAAAdXNlcl9pZAaLg2lNAAAAAAcAAABleHBpcmVzCgd0ZXN0MTAwBQAAAGxvZ2lu--6bdbaee00738ed3c2793d518857cfc9b"

И получили мы это приблизительно ( реальный код можно посмотреть в Mojolicious::Sessions и Mojolicious::Controller ) таким образом:

use Storable qw/freeze/;
use Mojo::Util qw/b64_encode hmac_md5_sum/;

sub store_session {
    # Get controller instance
    my $self = shift;
    
    # Get session hashref
    my $session = $self->stash('mojo.session');  

    # Set session expiration
    my $expires = time + $self->default_expiration();
    $session->{expires} = $expires;
    
    # Serialize session
    $value = b64_encode( freeze($session) );
    $value =~ s/\=/\-/g;
    
    # Secret
    my $secret = $self->app->secret;
  
    # Sign session
    my $signature = hmac_md5_sum( $value, $secret );
    $value .= "--$signature";
    
    # Create cookie
    my $cookie = $self->cookie('mojolicious', $value, { expires => $expires });
}


Использование Storable для сериализации позволяет выиграть в скорости, но лишает нас возможности читать данные сессии на стороне клиента( при помощи javascript ). Использование формата JSON  дало бы нам эту возможность.

UPD: Если работа с сессией предполагается на двух или более серверах, Storable принесет за собой еще один подводный камень. Если по какой-то причине версии этого модуля на машинах станут разными, данные перестанут распаковываться. Об этом придется помнить всегда.

Предыдущие посты про Mojolicious:

9 коммент.:

Анонимный комментирует...

Если работа с сессией предполагается на двух или более серверах, Storable принесет за собой еще один подводный камень. Если по какой-то причине версии этого модуля на машинах станут разными, данные перестанут распаковываться. Об этом придется помнить всегда.

koorchik комментирует...

> Если работа с сессией предполагается на двух или более серверах, Storable принесет за собой еще один подводный камень. Если по какой-то причине версии этого модуля на машинах станут разными, данные перестанут распаковываться. Об этом придется помнить всегда.

Спасибо, дельное замечание, добавил в пост

alienator комментирует...

> Использование Storable для сериализации позволяет выиграть в скорости

JSON::XS быстрее, и его не надо будет base64

> $value = b64_encode( freeze($session) );

Если уж беспокоиться о переносимости сессий между машинами, то лучше им было взять nfreeze()

alienator комментирует...

Что касается вообще сессий на клиенте, то мне плюсы (REST + ненужность серверного хранилища) кажутся сильно небольшими по сравнению с минусами.

Первое — трафик. То, что сессия должна влазить в 4Кб (за вычетом других кук, кстати), это полбеды. Пусть даже она будет на 300 байт больше, чем обычный идентификатор.

Так вот, страничка этого блога требует 11 реквестов. И с каждым идет красивая кука, хотя ее не просят. 3300 байт. Миллион хитов — сто гигов в месяц сверху. Оно надо? Можно, правда, path потюнить, но это не радикальное средство.

Второе, и очень важное.

> Поскольку сессии не хранятся на сервере, то нельзя просто взять и удалить файл

Очень зря. Как я понимаю, это не дает возможности сделать logout everywhere в случае дискредитации аккаунта или по желанию пользователя. Это неприятный косяк.

koorchik комментирует...

> Так вот, страничка этого блога требует 11 реквестов. И с каждым идет красивая кука, хотя ее не просят. 3300 байт. Миллион хитов — сто гигов в месяц сверху. Оно надо? Можно, правда, path потюнить, но это не радикальное средство.

Если это стает проблемой, то нужно вынести статику в отдельный домен.
Посмотрел( через firebug ) на запросы этой странички - их 47 штук(если страница не была еще закеширована), и с 47 всего 2 запроса пришлось на домен koorchik.blogspot.com, то есть кука с сессией передается всего 2 раза. Общий размер трафика - 455 кбайт. Если сессия 300 байт, то ее относительный размер составит 0,06% трафика. Что совершенно несущественно.

> Очень зря. Как я понимаю, это не дает возможности сделать logout everywhere в случае дискредитации аккаунта или по желанию пользователя. Это неприятный косяк.

Косяк. Но если нужен logout everywhere, то это можно реализовать следующим образом: хранить некое связанное с пользователем число, которое при логине сохраняется в сессию, если число у юзера и в сесси не совпадает, то сессия недействительна.

alienator комментирует...

> нужно вынести статику в отдельный домен

Ну да, домены и path-ы. Конечно, it depends, но, например, для мобильных версий все равно проблема. Там каждый килобайт на счету.

> хранить некое связанное с пользователем число

То есть хранить state на сервере, ура-ура :)

koorchik комментирует...

> То есть хранить state на сервере, ура-ура :) По большому счету, любые данные пользователя можно рассматривать как его состояние. А хранение некого числа связанного с пользователем это лишь еще один вид данных. Вся выгода в том, что это число не обязано быть доступным на всех серверах.

Мы можем реализовать шардинг и разбросать сервера по всему миру, и не переживать за общее хранилище сессий.

Ну а если в общем, то все конечно зависит от задач :)

koorchik комментирует...

2alienator
> JSON::XS быстрее, и его не надо будет base64
Просто Mojolicious без внешних зависимостей, но воможно кто-то еще реализует плагин для сессий с использованием JSON::XS. Кроме того Storable позволяет в сессию сохранить блеснутый объект.

>Если уж беспокоиться о переносимости сессий между машинами, то лучше им было взять nfreeze()

Cам был удивлен тем, что используется freeze :)

good code комментирует...

Хорошая статья... Как раз решаю этот вопрос. Но комменты убедили меня хранить сессии в Mysql.memory

Отправить комментарий

Не забудьте добавить себя в постоянные читатели и включить уведомления о новых комментариях, либо воспользуйтесь RSS каналом ;)