Передача контекста между сервисами без базы данных

В распределённых JavaScript-системах состояние часто становится источником сложности: необходимость синхронизации, обращения к общей базе данных, управление сессиями и согласованность данных между сервисами резко увеличивают связность архитектуры. Один из способов уйти от централизованного хранения контекста — переносить его в защищённом виде вместе с запросом, используя криптографически «запечатанные» структуры данных.

Библиотека @hapi/iron решает задачу безопасного упаковывания произвольного JavaScript-объекта в строку, которую можно передавать между сервисами или хранить на клиенте без риска изменения содержимого. Такой подход позволяет реализовать передачу контекста без базы данных, сохраняя целостность и конфиденциальность данных.

Iron реализует концепцию sealed box: объект сериализуется, шифруется и снабжается механизмом проверки целостности. В результате получается строка, которая:

  • не читается без секретного ключа;
  • не может быть изменена незаметно;
  • может содержать TTL (время жизни);
  • может быть восстановлена обратно в исходный объект.

Процесс состоит из двух этапов:

  1. Сериализация и шифрование — данные преобразуются в строку и шифруются симметричным алгоритмом.
  2. Защита целостности — добавляется HMAC-подпись, предотвращающая подмену.

Таким образом, Iron объединяет конфиденциальность и защиту от модификации в одном формате.

Базовое использование Iron

Основные операции библиотеки — seal и unseal.

Упаковка данных

import Iron from '@hapi/iron';

const password = 'super-secure-password';

const session = {
    userId: 42,
    role: 'admin',
    permissions: ['read', 'write']
};

const sealed = await Iron.seal(session, password, {
    ttl: 60 * 60 * 1000 // 1 час
});

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

Распаковка данных

const unsealed = await Iron.unseal(sealed, password, {
    ttl: 60 * 60 * 1000
});

console.log(unsealed.userId); // 42

Если строка была изменена или срок жизни истёк, операция завершится ошибкой.

Передача контекста между сервисами

В микросервисной архитектуре каждый сервис стремится быть автономным. Однако часто требуется передавать контекст пользователя, например:

  • идентификатор пользователя;
  • права доступа;
  • текущую организацию или тенант;
  • параметры запроса.

Использование Iron позволяет передавать этот контекст без обращения к централизованной базе данных или Redis-сессиям.

Передача через HTTP-заголовки

Контекст может быть упакован в строку и отправлен в заголовке:

// gateway service
const contextToken = await Iron.seal({
    userId: 10,
    tenantId: 'acme',
    scope: ['billing']
}, password);

fetch('https://orders.service.local/create', {
    method: 'POST',
    headers: {
        'x-context': contextToken
    }
});

На стороне сервиса:

const token = req.headers['x-context'];

const context = await Iron.unseal(token, password);

if (context.scope.includes('billing')) {
    // разрешаем доступ
}

Таким образом каждый сервис получает полный набор необходимых данных без обращения к внешнему хранилищу.

Stateless-аутентификация и отказ от сессий

Традиционные системы используют серверные сессии, которые требуют хранения состояния. Iron позволяет заменить их полностью статeless-подходом:

  • сервер не хранит сессии;
  • клиент передаёт зашифрованный контекст;
  • доверие обеспечивается криптографией.

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

Отличие Iron от JWT

Хотя Iron и JSON Web Token решают похожие задачи, их подход различается:

  • JWT — это структурированный токен с отдельной подписью и полезной нагрузкой в открытом виде (если не используется JWE).
  • Iron всегда шифрует данные полностью.
  • Iron скрывает структуру объекта полностью, включая ключи.
  • JWT чаще используется для интеграций между системами.
  • Iron чаще применяется внутри доверенной инфраструктуры (например, в Hapi-экосистеме).

Ключевое отличие — уровень скрытия данных: Iron по умолчанию делает содержимое недоступным без расшифровки.

Контекст как единый объект домена

Передача контекста через Iron позволяет формировать единый объект состояния запроса:

{
    user: {
        id: 10,
        name: 'Alex'
    },
    auth: {
        role: 'editor'
    },
    request: {
        correlationId: 'abc-123'
    }
}

Такой объект может сопровождать запрос через все сервисы цепочки, не требуя повторной авторизации и дополнительных запросов к базе.

Использование в очередях сообщений

Контекст можно передавать не только через HTTP, но и через брокеры сообщений:

const message = {
    payload: orderData,
    context: await Iron.seal(userContext, password)
};

queue.publish('orders.created', message);

Консьюмер восстанавливает контекст:

const context = await Iron.unseal(message.context, password);

Это позволяет сохранять идентичность запроса в асинхронных потоках обработки.

Управление ключами и безопасность

Безопасность Iron полностью зависит от секретного ключа. В реальных системах важно учитывать:

  • ключ не должен храниться в коде;
  • желательно использовать переменные окружения или vault;
  • возможна ротация ключей;
  • разные сервисы могут использовать разные уровни доступа.

Пример использования массива ключей:

const password = [
    process.env.IRON_KEY_CURRENT,
    process.env.IRON_KEY_PREVIOUS
];

Это позволяет расшифровывать старые токены после смены ключа.

TTL и ограничение времени жизни контекста

Каждый sealed-объект может иметь срок жизни:

{
    ttl: 5 * 60 * 1000 // 5 минут
}

После истечения времени попытка восстановления приведёт к ошибке. Это предотвращает повторное использование устаревших контекстов и снижает риск replay-атак.

Распространённые архитектурные сценарии

Gateway как точка упаковки контекста

API-шлюз формирует контекст один раз и передаёт его дальше:

  • аутентификация пользователя;
  • формирование прав доступа;
  • упаковка через Iron;
  • проксирование запросов.

Service Mesh без централизованной сессии

Каждый сервис:

  • не хранит состояние;
  • доверяет входящему sealed-контексту;
  • при необходимости добавляет новые поля и повторно упаковывает данные.

Корреляция запросов

Контекст может включать correlationId, что позволяет:

  • отслеживать цепочки вызовов;
  • логировать распределённые транзакции;
  • строить трассировку без внешнего состояния.

Ограничения подхода

Несмотря на удобство, использование Iron для передачи контекста требует учёта ограничений:

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

Тем не менее в системах, ориентированных на stateless-архитектуру, эти ограничения компенсируются снижением инфраструктурной сложности.

Применение в реальных системах

Iron чаще всего используется:

  • в Hapi-приложениях для cookie-based сессий;
  • в микросервисах с единым API gateway;
  • в системах, где требуется безопасная передача пользовательского контекста;
  • в архитектурах без централизованного session storage.

В таких сценариях он выступает как слой безопасной сериализации состояния, позволяющий отказаться от базы данных как источника сессионного контекста.