Comments 107
Я понимаю, конечно, что много всего держать в сессии плохо, но как работать с ограничением 4КБ на 1 куку?
Нет ли смысла в данном случае предусмотреть в том числе сохранение данных в SessionStorage?
То, что вы придумали обычно реализуют через JWT, для чего есть множество библиотек, в том числе на PHP.
А ещё есть статьи где популярно объясняется почему реализация сессий на клиенте это плохая идея в общем и с использованием JWT в частности:
https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-bad-standard-that-everyone-should-avoid
http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
Ваша реализация, судя по тому что я прочел по диагонали, идеологически похожа, так что вторая статья (и её follow-up) весьма применима и для вашего случая.
Отмечу, что перечисленные недостатки относятся исключительно к использованию JWT в качестве хранилища сессионных данных.
Вы правда читали статью по второй ссылке?
Не зависимо от того, используется JWT или другой способ шифрованного хранения сессионных данных, все претензии к подходу из статьи остаются в силе. А первая ссылка на случай если кто-то решит использовать JWT как таковой в принципе.
Да, читал. В обоих статьях большая часть — бред и паника на пустом месте.
Тогда будьте добры, уточните что во второй статье относится исключительно к JWT и не релевантно данной статье.
Данной статье оно как раз очень даже релевантно. Оно не релевантно для использования JWT в тех задачах, для которых JWT создавался.
- They take up more space
- You cannot invalidate individual JWT tokens
- Data goes stale
- Implementations are less battle-tested or non-existent»
Каждый пункт как-то отражен в данной статье, автор про это задумывался. Так что корректно было бы обсуждать каждый момент по существу, а не просто кидать ссылку.
Ссылку я кинул для того, чтобы не повторять то, что кто-то уже структурировал в виде статьи. Задумываться над этим мало, нужно иметь решение к вопросам. Но исходя из их сущности решения скорее всего быть не может в общем случае. Если считаете иначе — давайте обсуждать.
Опять таки, чтобы не повторяться, рекомендую прочесть саркастический ответ на часто встречаемые контр-аргументы (он упомянут в шапке той статьи, если пропустили): http://cryto.net/~joepie91/blog/2016/06/19/stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work/
Надо стремиться использовать наиболее подходящее решение в каждом случае и исправлять дефекты, но заявлять «JWT sucks», как авторы тех статей — неправильно.
JWT или другой способ шифрованного хранения сессионных данных
В JWT, кстати, данные хранятся в открытом виде.
Не обязательно
Вы Abstract читали (первый параграф)?:
JSON Web Token (JWT) is a compact, URL-safe means of representing
claims to be transferred between two parties. The claims in a JWT
are encoded as a JSON object that is used as the payload of a JSON
Web Signature (JWS) structure or as the plaintext of a JSON Web
Encryption (JWE) structure, enabling the claims to be digitally
signed or integrity protected with a Message Authentication Code
(MAC) and/or encrypted.
Весьма однозначно сказано что содержимое вполне себе может быть зашифрованным.
The contents of the JOSE Header describe the cryptographic operations applied to the JWT Claims Set. If the JOSE Header is for a JWS, the JWT is represented as a JWS and the claims are digitally signed or MACed, with the JWT Claims Set being the JWS Payload. If the JOSE Header is for a JWE, the JWT is represented as a JWE and the claims are encrypted, with the JWT Claims Set being the plaintext encrypted by the JWE. A JWT may be enclosed in another JWE or JWS structure to create a Nested JWT, enabling nested signing and encryption to be performed.
— https://tools.ietf.org/html/rfc7519#page-6
Возможно безопасно и надёжно хранить данные сессии в браузерной куке у самого пользователя, если заверить данные сессии криптографической подписью.
… только до тех пор, пока "данные сессии" — маленькие. В противном случае вы получаете гигантский оверхед на каждом запросе.
Как решается вопрос принудительной инвалидации сессии?
А вообще, по-моему, данный подход не является сессией в обычном смысле слова в веб-разработке. Сессия — хранение каких-то данных на стороне сервера такое, что результаты запросов клиента зависят от истории предыдущих запросов. Тут же каждый запрос получается самодостаточным, что-то вроде WebForms.
А WebForms-то тут при чем?..
Может запамятовал, но вроде они отправляли с фронта на сервер всё состояние сессии, ну и получали его от сервера.
Они отправляли на сервер все состояние страницы. Сессия работала отдельно.
Они отправляли состояние страницы, а не сессии. Если у пользователя открыты сразу две вкладки — эти понятия принципиально различаются.
Кроме того, ViewState не сохраняется при переходах между страницами.
А он никак не решается. Разве что костылем в виде хранилища отозванных сессий.
Как и указано в недостатках,
Клиент может откатить состояние сессии на любое выданное и подписанное ранее значение, криптоподпись которого ещё действительна в текущий момент времени.Однако здесь важно заметить: если, к примеру, стоит практическая задача разлогинить юзера, то инвалидация одной сессии сама по себе ничего не даёт. Нужно прервать все сессии. И здесь у классического подхода к хранению сессий тоже возникают проблемы, так как в таком случае нужно найти и удалить все сессии, которые принадлежат пользователю. То есть это значит нужно хранить и поддерживать соответствие пользователя списку сессий. Дальше — больше: если используются sticky-сессии, то тогда один сервер должен пошариться по множеству других, чтобы разлогинить юзера через удаление сессий.
Удаление сессий в любом случае непрактичный подход. Вместо этого в сессию записывают факт логина вместе со временем логина. А в профиле пользователя держат время последнего логаута. Если время логина сессии раньше времени последнего логаута — считать её неактивной. При таком подходе для логаута нужно всего лишь обновить одно значение в записи о пользователе.
А в профиле пользователя держат время последнего логаута.
И как, бишь, вы это время между разными серверами будете синхронизировать в случае sticky sessions?
(и как вы будете решать задачу "разлогинь меня в этой сессии, не трогая остальные"?)
И как, бишь, вы это время между разными серверами будете синхронизировать в случае sticky sessions?Время логаута не нужно синхронизировать, оно в профиле пользователя
(и как вы будете решать задачу «разлогинь меня в этой сессии, не трогая остальные»?)Если нужно предоставить пользователю возможность разлогинить любую сессию, на которую он покажет пальчиком, то и при том и том подходе нужно хранить список сессий. Если только ту, в которой пользователь прямо сейчас сидит, то у классического подхода тут преимущество. Обязательно пользуйтесь классическим подходом, если Ваша бизнес-задача — разлогинивать текущие сессии пользователей.
Время логаута не нужно синхронизировать, оно в профиле пользователя
… который хранится где?
Если нужно предоставить пользователю возможность разлогинить любую сессию, на которую он покажет пальчиком, то и при том и том подходе нужно хранить список сессий.
После чего совершенно не понятно, зачем гонять данные сессии на клиента.
… который хранится где?Который хранится в базе, а не в сессии.
После чего совершенно не понятно, зачем гонять данные сессии на клиента.В таком случае заведите ишью в репозиториях джанги и рельсов, потребуйте немедленно удалить непонятные бэкенды для сессий.
Который хранится в базе, а не в сессии.
… которая становится той же самой единой точкой отказа. В чем разница-то?
Так что мешает хранить сессии в той же БД, что и профили? Как была одна точка отказа, так и осталась.
Да.
- Как вы удаляли старые сессии?
- Каков ваш план масштабирования пропускной способности базы на запись при росте числа сессий?
Как вы удаляли старые сессии?
Регулярной задачей.
Каков ваш план масштабирования пропускной способности базы на запись при росте числа сессий?
Шардинг.
Напоминаю при этом, что подход с хранением сессий в куках сам по себе не полагается на базу, если приложение не использует базу вовсе, то и для хранения сессий она не нужна.
… напоминаю, что подход с хранением сессий в куках работает только при малом объеме данных в сессии.
Автор статьи спросил бы у вас: Допустим, memcache упал/втупил/стал недоступен. У вас или лежит сайт, или все незалогинены.
От себя замечу, что если вам хотя бы чуть-чуть важны залогиненые пользователи, под сессии лучше использовать redis.
Там есть и сохранение на диск, и реплики, и кластер.
Я не использовал Redis в highload пректе и не могу лично от себя ничего сказать…
Да, в memcache прикрутили что-то для репликации и можно использовать несколько серверов memcached в качестве кластера.
Но данные при перезагрузке ноды вы все равно потеряете.
В highload redis отлично используется для хранения сессий, к нему обращаются php, python, go.
По вашей ссылке это проблема не redis, а конкретного php extension.
Там в обсуждении люди описали, как сделать свой handler с session locking.
Я не программист, но лично мне кажется, что session locking нужно делать в приложении, а не внутри extension, так как это специфичная фича — каждый может видеть её реализацию по разному.
На некоторой стадии развития веб-проекта возникает одна из следующих ситуаций:
backend перестаёт помещаться на одном сервере и требуется хранилище сессий, общее для всех backend-серверов
по различным причинам перестаёт устраивать скорость работы встроенных файловых сессий
Традиционно в таких случаях для хранения пользовательских сессий начинают использовать Redis, Memcached или какое-то другое внешнее хранилище. Как следствие возникает бремя эксплуатации базы данных, которая при этом не должна быть единой точкой отказа или бутылочным горлышком в системе.
Я вот никогда раньше и не задумывался, что можно так сессии хранить. Теперь обдумываю…
Замечу, что большие куки — это плохо. Несколько раз у разных подсистем (не php) ловили падения при большом объеме кук. Ловить такие ошибки на проде — всегда очень занимательная задача.
Для того, чтобы перенести хранение сессий на сторону клиента без изменения механизма работы с ними.
В памяти хранить сложно, если у вас несколько инстансов апп-сервера — нужен механизм репликации (причём мастер-мастер) памяти между серверами.
И все таки проще и логичнее просто перейти на работу с куками без сессий. С учетом того, что во многих фрейморках почти всега используются классы обертки для работы с сессиями. Просто в них использовать данные из куков. Но как пример работы через сессий через куки — довольно хороший)
Начало поста:
Традиционно в таких случаях для хранения пользовательских сессий начинают использовать Redis, Memcached или какое-то другое внешнее хранилище. Как следствие возникает бремя эксплуатации базы данных, которая при этом не должна быть единой точкой отказа или бутылочным горлышком в системе.
Однако, есть альтернатива этому подходу...
Не факт, что проще и логичней, не очень хорошо и в классы-обёртки сессий фреймворков лезть, и переписывать код, если $_SESSION используется.
Синхронизация между устройствам? Вот вы зашли на сайт с рабочего компа, затем в транспорте с планшета (еще и с 3G), что будет?
1. HMAC — это не электронная подпись. Поскольку строится на базе симметричного ключа и соответственно может быть подделана одной стороной без возможности это опровергнуть другой стороной. Классическая ЭП на ассиметричной криптографии этого не допускает, поскольку ЭП строится с помощью закрытого ключа который есть только у подписывающей стороны.
2. В приведенных вами исходниках ключ шифрования вшит в код, а с учетом того, то шифрование должно осуществляться на стороне клиента этот ключ может быть перехвачен третьей стороной и после чего данные будут подделаны.
Пример. Вася подключается в Web-клиенту Банка. К нему грузятся скрипты, содержащие ключ шифрования.
Вася отключается, но ключ у него есть. Вася знает что у Зины на счету много денег. Он от имени Зины строит сессию (ему даже авторизовываться не надо) и совершает перевод.
3. Вы скорее всего скажите, что сессия будет защищена TLS. Но раз так, тогда смысла в доп. шифровании нет.
Для исправления ситуации необходимо как минимум для каждой сессии получать рандомный ключ шифрования. Но даже это не защитит от атак повтором. Поэтому нужно капать в сторону использования ассиметричной крипты.
Вы пост-то вообще читали? Сторона тут только одна — сервер, который сначала генерирует куку, а потом ее же читает.
Если только некий id, тогда непонятно причем тут ограничения на 4кб в куку, если же в сессии присутствует состояние полное состояние клиента, то для того чтобы его нехранить на сервере его надо шифровать и оно должно шифроваться на клиенте.
В статье схемы взаимодействия не хватает.
Веб-сессия пользователя, оно же сеанс — это понятие, объеденяющее активность пользователя по взаимодействию с веб-ресурсом с момента первого обращения к ресурсу и до прекращения взаимодействия ресурсом.
Обычно под сессией понимают термин "состояние сеанса", означающий блок нетипизированных данных, хранимых где-нибудь в течении всего сеанса, и к которым имеет доступ только серверный код.
Клиентскому же коду доступ к сессии не нужен — у него есть localStorage для тех же целей.
Я, например, генерировал id сессии хешем от юзерагента, ip, даты и секретного ключа.
И не сохранял в куках, а просто генерировал при каждом запросе.
Даты чего?
Даты создания сессии
Я не понимаю. Где вы храните дату создания сессии, если id сессии генерируется на ее основе?
Дату берем сегодняшнюю.
Отлично. Теперь любые два пользователя, которые сидят с одного IP и одного браузера в один день имеют общую сессию!
Вы что за сайты-то делаете, если ваша целевая аудитория, по всей видимости, бездетные незамужние люди, не сидящие за NATом?..
PS Пост в тему — https://habrahabr.ru/post/108493/
Браузер обновился — поменялся хэш. Надо брать не весь юзер агент, а из него платформу + ip + time zone,
Я прицельно искал такие реализации в поиске хабра, но не смог найти Вашу статью. Но сейчас я вижу, что даже найдя её, я бы её ни в коем случае не выбрал.
Во-первых, IV это не публичный ключ, и хранить его отдельно от сообщения нет смысла.
Во-вторых, нет подписи куки, и я писал в статье, почему она должна быть.
1. Имеет, инвалидация же.
2. Шифрование же.
И что? Клиент-то ее получил и прочитал, значит, и обратно послать может. Все эти флажки "безопасности" — они для добронамеренных клиентов.
Куки с флагом HttpOnly не видны браузерному коду
Это, простите, как? А отправляет их кто?
Куки с флагом Secure пересылается только через HTTPS (HTTP с использованием SSL — Secure Socket Level)
Только если клиент выполняет это требование. И да, даже если клиент выполняет это требование, он может не выполняеть требование про валидацию сертификатов, или сертификаты могут быть подменены на уровне устройства и так далее, далее, далее.
Так вот, клиенты браузерами не ограничиваются. Поэтому если у нас есть способ куку перехватить (а если его нет, то все обсуждения, в общем-то, на пустом месте), то мы всегда можем ее отправить и сделать подмену сессии.
Куки для API?
Нет, для подконтрольного агента.
Даже если куки для API, то они защищены ssl, а значит расшифровать перехваченный трафик вы всеравно не можете
Еще раз говорю: если у вас есть способ передавать куки с гарантированной невозможностью перехвата, то вы можете их использовать для чего угодно и как угодно. Инвалидация, подписание и прочий цирк нужен, когда такого способа нет по тем или иным причинам.
Инвалидация нужна не только для защиты от компрометации кук и прочих токенов, но и для защиты от пользователя, который перестал быть доверенным.
Для этого надо пользователя инвалидировать целиком.
Пользователя инвалидировать не проблема, проблема инвалидировать все его открытые сессии, если принято решение на каждый запрос не проверять не запрещён ли с момента последнего запроса вход пользователю.
Да, единая точка отказа и бутылочное горлышко в виде редиса/мемкшеа есть, но уже существует множество решений ждя больших объемов данных
Справедливости ради, стоит отметить, что в Laravel один из драйверов сессий, которые идут с фреймворком по умолчанию, предназначен именно для хранения данных сессии в куках.
https://laravel.com/docs/master/session
- Громоздкая сериализация: ключи в жсон прям текстом пишутся, от всех полей в base64 паддинг ("===") остаётся, могли бы трансляцию в urlsafe base64 сделать, подпись прям в строковом виде. Можно было бы снизить расходы этой части раза в полтора точно.
- Экспайр не охвачен подписью, то есть сессии — всегда вечные.
В остальном всё очень хорошо сделано.
Состояние может быть очень сложным. Дополнительные сложности в отладке ради экономии 1 запроса, но платить за это жирнющей кукой — сомнительная выгода.
На момент написания статьи я уже был с ним знаком, но не включил его в обзор по той причине, что он является не хандлером сессий, а middleware для сообщений PSR-7 (по сути аналог фильтра в UNIX-конвейере). То есть подходит не кому угодно, а тем, у кого всё приложение выстроено как обработчик сообщений PSR-7.
Если по этому критерию он подходит, то вот особенности данной реализации. Не вчитывался слишком вдумчиво, так как не работал с PSR-7, но обнаружил:
- Нет шифрования.
- Используется формат вебтокена JWT. Вместе с этим в качестве сериализатора всегда используется не один из встроенных сериализаторов сессий PHP, а всегда json_encode. Строго говоря, он может сериализовать не всё то же самое, что может сериализовать каждый из внутренних сериализаторов сессий PHP, нужно это учитывать. Кроме того, сама по себе кука получается размером ещё больше по сравнению со всеми остальными реализациями — оверхед максимальный.
- Исходя из использования JWT, помимо HMAC-подписи доступна ещё RSA-подпись. Это, наверное, плюс, но я не вижу юзкейса: подписывает и проверяет одна и та же сторона, она же имеет оба ключа. В таком случае намного оптимальнее отказаться от асимметричной криптографии для подписи в пользу HMAC — и по соображениям простоты, и по соображениям скорости.
Про всё это (плюс про то, что я писал в статье) и так в документации написано.
Одно место я так и не понял: https://github.com/psr7-sessions/storageless/blob/master/src/Storageless/Http/SessionMiddleware.php#L305-L311 — не знаю, означает ли это захардкоженный лимит времени жизни куки (и сессии) или нет.
PHP: Хранение сессий в защищённых куках