От переводчика: Это третья статья из цикла о Node.js от команды Mozilla Identity, которая занимается проектом Persona. Эта статья посвящена применяемому в Persona способу хранения данных сессии на клиенте.
Статические веб-сайты хорошо масштабируются. Их легко кэшировать, и не нужно постоянно синхронизировать данные на нескольких серверах.
К сожалению, большинство веб-приложений должны хранить информацию о состоянии, чтобы предлагать пользователям персонализированные страницы. Если пользователи могут регистрироваться на сайте, то нам надо хранить сессии. Самый распространенный способ — установить cookie со случайным идентификатором сессии, а детали хранить на сервере.
Если необходимо масштабировать такой сайт, есть три варианта:
У всех этих подходов есть недостатки:
Тем не менее, поразмыслив немного, можно придумать и четвёртый способ: хранить все данные сессии на клиенте.
У хранения данных сессии в браузере есть несколько очевидных преимуществ:
Но есть одна большая проблема: данным, хранящимся у клиента, нельзя доверять. К примеру, если вы храните в cookie ID пользователя, то он может подменить его и получить доступ к чужой учётной записи.
Хоть эта проблема и кажется непреодолимой, у неё есть решение: хранить данные в защищённом контейнере. Таким образом, больше нет необходимости доверять клиенту — у него нет возможности незаметно их подменить.
На практике это означает, что данные в cookie надо зашифровать и подписать ключом, хранящимся на сервере. Именно этим занимается модуль client-sessions.
Библиотека node-client-sessions для Node.js заменяет стандартные middleware-модули фреймворка Connect session и cookie-parser. Вот как включить её в простое приложение для Express:
Затем можно устанавливать значения свойств объекта
и считывать их:
Для закрытия сессии служит метод
Один из главных недостатков хранения сессий на клиенте состоит в том, что сервер больше не может закрывать сессии самостоятельно.
При хранении данных сессии на сервере достаточно просто удалить данные сессии из БД, после чего все cookie на всех клиентах буду указывать на несуществующую запись. Когда данные хранятся на клиенте, сервер не может быть уверен, что данные были удалены во всех браузерах. Другими словам, не так то просто синхронизировать новое состояние сервера (пользователь завершил сессию) с состоянием на клиенте (сессия открыта).
Чтобы немного снизить остроту проблемы, client-sessions хранит в cookie время жизни сессии. Прежде чем распаковывать данные из зашифрованного контейнера, сервер проверит, не просрочены ли они. Если да, то он проигнорирует эти данные и будет считать, что сессия закрыта.
Хотя схема с установкой времени жизни работает неплохо, особенно, если время не очень большое, в случае с Persona нам нужен был способ немедленно закрывать сессию на всех клиентах, если пользователь менял пароль.
Нам всё же пришлось хранить малую толику состояния на сервере. Мы сделали это, добавив одно поле в таблицу БД на сервере и в cookie.
Каждый вызов API, который обращается к данным сессии, теперь считывает это поле в БД и сравнивает его с аналогичным полем в cookie. Если они не совпадают, сессия считается закрытой.
Лишнее обращение к БД — это, конечно не очень хорошо, но, к счастью, нам и так приходилось читать из таблицы с данными пользователей при почти каждом вызове, так что идентификатор сессии можно было получить без лишних накладных расходов. Чтобы вы могли быстрее начать экспериментировать с модулем, мы написали небольшое демонстрационное приложение.
Все статьи цикла:
- "Охотимся за утечками памяти в Node.js"
- "Нагружаем Node под завязку"
- "Храним сессии на клиенте, чтобы упростить масштабирование приложения"
- "Производительность фронтэнда. Часть 1 — конкатенация, компрессия, кэширование"
- "Пишем сервер, который не падает под нагрузкой"
- "Производительность фронтэнда. Часть 2 — кешируем динамический контент с помощью etagify"
- "Приручаем конфигурации веб-приложений с помощью node-convict"
- "Производительность фронтенда. Часть 3 — оптимизация шрифтов"
- "Локализация приложений Node.js. Часть 1"
- "Локализация приложений Node.js. Часть 2: инструментарий и процесс"
- "Локализация приложений Node.js. Часть 3: локализация в действии"
- "Awsbox — PaaS-инфраструктура для развёртывания приложений Node.js в облаке Amazon"
Статические веб-сайты хорошо масштабируются. Их легко кэшировать, и не нужно постоянно синхронизировать данные на нескольких серверах.
К сожалению, большинство веб-приложений должны хранить информацию о состоянии, чтобы предлагать пользователям персонализированные страницы. Если пользователи могут регистрироваться на сайте, то нам надо хранить сессии. Самый распространенный способ — установить cookie со случайным идентификатором сессии, а детали хранить на сервере.
Масштабирование сайта с хранением состояния
Если необходимо масштабировать такой сайт, есть три варианта:
- Реплицировать данные сессии между всеми серверами.
- Использовать центральное хранилище, к которому будут обращаться все серверы.
- Закрепить за каждым пользователем определённый сервер.
У всех этих подходов есть недостатки:
- Репликация ухудшает производительность и увеличивает сложность.
- Центральное хранилище ограничивает возможность масштабирования и приводит к дополнительным задержкам.
- Привязка пользователей к конкретным серверам приводит к проблемам, когда сервер отключается.
Тем не менее, поразмыслив немного, можно придумать и четвёртый способ: хранить все данные сессии на клиенте.
Хранение сессий на клиенте
У хранения данных сессии в браузере есть несколько очевидных преимуществ:
- Данные доступны всегда, независимо от того, какой сервер обслуживает клиента.
- Не надо хранить состояние на сервере.
- Не надо синхронизировать информацию о состоянии между серверами.
- Можно очень легко добавлять новые серверы.
Но есть одна большая проблема: данным, хранящимся у клиента, нельзя доверять. К примеру, если вы храните в cookie ID пользователя, то он может подменить его и получить доступ к чужой учётной записи.
Хоть эта проблема и кажется непреодолимой, у неё есть решение: хранить данные в защищённом контейнере. Таким образом, больше нет необходимости доверять клиенту — у него нет возможности незаметно их подменить.
На практике это означает, что данные в cookie надо зашифровать и подписать ключом, хранящимся на сервере. Именно этим занимается модуль client-sessions.
node-client-sessions
Библиотека node-client-sessions для Node.js заменяет стандартные middleware-модули фреймворка Connect session и cookie-parser. Вот как включить её в простое приложение для Express:
const clientSessions = require("client-sessions");
app.use(clientSessions({
secret: '0GBlJZ9EKBt2Zbi2flRPvztczCewBxXK' // set this to a long random string!
}));
Затем можно устанавливать значения свойств объекта
req.session
:app.get('/login', function (req, res){
req.session.username = 'JohnDoe';
});
и считывать их:
app.get('/', function (req, res){
res.send('Welcome ' + req.session.username);
});
Для закрытия сессии служит метод
reset()
:app.get('/logout', function (req, res) {
req.session.reset();
});
Мгновенное закрытие сессий Persona
Один из главных недостатков хранения сессий на клиенте состоит в том, что сервер больше не может закрывать сессии самостоятельно.
При хранении данных сессии на сервере достаточно просто удалить данные сессии из БД, после чего все cookie на всех клиентах буду указывать на несуществующую запись. Когда данные хранятся на клиенте, сервер не может быть уверен, что данные были удалены во всех браузерах. Другими словам, не так то просто синхронизировать новое состояние сервера (пользователь завершил сессию) с состоянием на клиенте (сессия открыта).
Чтобы немного снизить остроту проблемы, client-sessions хранит в cookie время жизни сессии. Прежде чем распаковывать данные из зашифрованного контейнера, сервер проверит, не просрочены ли они. Если да, то он проигнорирует эти данные и будет считать, что сессия закрыта.
Хотя схема с установкой времени жизни работает неплохо, особенно, если время не очень большое, в случае с Persona нам нужен был способ немедленно закрывать сессию на всех клиентах, если пользователь менял пароль.
Нам всё же пришлось хранить малую толику состояния на сервере. Мы сделали это, добавив одно поле в таблицу БД на сервере и в cookie.
Каждый вызов API, который обращается к данным сессии, теперь считывает это поле в БД и сравнивает его с аналогичным полем в cookie. Если они не совпадают, сессия считается закрытой.
Лишнее обращение к БД — это, конечно не очень хорошо, но, к счастью, нам и так приходилось читать из таблицы с данными пользователей при почти каждом вызове, так что идентификатор сессии можно было получить без лишних накладных расходов. Чтобы вы могли быстрее начать экспериментировать с модулем, мы написали небольшое демонстрационное приложение.
Все статьи цикла:
- "Охотимся за утечками памяти в Node.js"
- "Нагружаем Node под завязку"
- "Храним сессии на клиенте, чтобы упростить масштабирование приложения"
- "Производительность фронтэнда. Часть 1 — конкатенация, компрессия, кэширование"
- "Пишем сервер, который не падает под нагрузкой"
- "Производительность фронтэнда. Часть 2 — кешируем динамический контент с помощью etagify"
- "Приручаем конфигурации веб-приложений с помощью node-convict"
- "Производительность фронтенда. Часть 3 — оптимизация шрифтов"
- "Локализация приложений Node.js. Часть 1"
- "Локализация приложений Node.js. Часть 2: инструментарий и процесс"
- "Локализация приложений Node.js. Часть 3: локализация в действии"
- "Awsbox — PaaS-инфраструктура для развёртывания приложений Node.js в облаке Amazon"