PHP session handler compatible with Node express-session
Compatible PHP session handler
В современном мире никого не удивить сервис ориентированной архитектурой. Можно сказать что сейчас это уже не менйстрим, а формат разработки. Это не модно, это реально удобно, когда монолит разбит на разные составляющие и каждая задача решается тем инструментом, который лучше для этого подходит. И это справедливо уже не только для энтерпрайза, но и пет проджекты могут состоять из готовых докер контейнеров, реализующих небольшие сервисы.
Например, Node.js отлично справляется с I/O и отлично решает паттерн BFF (Backend for Frontend). Об этом подробнее говорили в недавнем выпуске RadioJS:
Но есть вещи, которые лучше (удобнее/быстрее) писать на других языках. Сейчас я переписываю наш проект GeekJOB.ru и у меня в одной из частей проекта есть воркфлоу, где фронтенд отдается через Node.js, при этом нода хранит сесси в Redis (мне кажется один из приемлемых способов для распределенного хранения сессий). Есть части сервиса, реализованные на PHP (в основном это различный процессинг загружаемых файлов, конвертации, сборка-разборка, вычленение текстов, передача данных на NLP обработку в NER модуль на Python, извлечение смыслов, обработка, etc…).
И вот встал вопрос шеринга сессий. Сессии в PHP и в Node.js формируются по разному и это вообще интересная тема. Сначала я поискал готовые модули, но я не нашел ничего адекватного(по моим меркам) что я мог бы взять в продакшен. Скажем так, во всех них был фатальный недостаток.
Одно из решений, что предлагают разные разработчики: просто брать на стороне PHP айдишник Node.js сессии и модулем Redis ходить в базу и получать данные. Это даже не про работу сессий, это чисто про прочитать данные по ID из базы. Изи? Вери изи. Но, это не механизм сессий, работает только в одностороннем порядке (имеется в виду не запись в базу, а генерация сессии на стороне PHP с последующим принятием ее в Node.js).
Смотрел в сторону готовых решений, но как выяснилось, они требуют допиливания. Кто как смог решить свою задачу — так и сделал. Некоторые библиотеки отпугнули своими реализациями.
Я же захотел разобраться как устроены эти сессии и привести их к единому формату, в итоге у меня получился PHP Session Handler, который регистрируется в PHP, после чего он умеет работать с Node.js express-session сессиями без каких-то костылей. Все очень прозрачно.
Причем PHP умеет не просто работать с готовой сессией от Node.js, но и генерить и выставлять правильную сессию, которую примет Node.js. И это очень важный шаг, потому, что если сессия будет неправильной, вы можете уронить Node.js приложение, так как express-session считает что данные нельзя испортить.
О чем я говорю, давайте поясню. Express кладет в базу сессию с блоком cookie:
Если этого блока или какого-то поля в объекте не будет, то все (матьевосукабля) приложение на Node.js упадет! И пока будет такая битая сессия в базе и на клиенте, приложение будет рестартовать в бесконечном цикле. Таким образом можно вывести из строя целую ноду (мы же деплоим на продакшен не 1 экземпляр сервиса).
А в логах вы увидите:
node_modules/express-session/session/store.js:87 var expires = sess.cookie.expires ^ TypeError: Cannot read property 'expires' of undefined at RedisStore.Store.createSession (node_modules/express-session/session/store.js:87:29) at node_modules/express-session/index.js:478:15
Это жесть. Для своего приложения я сделал фикс и заодно запулил PR:
Сессия состоит из префикса, идентификатора и подписи.
s:session_id.signature
Префикс простой “s:” тут ни отнять, ни добавить. Есть и есть, всегда один и тот же.
Подпись
Подпись гарантирует что нам пришла валидная сессия и ее никто не подделал по дороге к серверу. При настройке сессий есть поле secret key, которое как раз из себя представляет соль, добавляемую при хешировании:
const app = express()
app.use(session({ secret: 'some secret salt', }))
В модуле express-session можно найти формирование куки:
function setcookie(res, name, val, secret, options) { var signed = 's:' + signature.sign(val, secret);
В свою очередь модуль использует модуль cookie-signature:
Отлично. Значит, чтобы разобрать эту куку в PHP, нам достаточно выцепить Session ID, и даже можем не реализовывать проверку подписи. Но если хочется прям все по красоте, то реализуем и проверку на подделку.
PHP Session Handler
Раньше для регистрации обработчиков сессий приходилось писать отдельные функции. Но сейчас есть интерфейс и даже класс, от которого можно унаследоваться:
class ExpressjsSessionHandler extends SessionHandler {
В нем мы реализуем только необходимые методы, все остальное будет работать по дефолту.
Для работы с Redis использую модуль из PECL, который из коробки умеет работать с сессиями. Ставится очень просто:
pecl install redis
В Docker на базе официальной репы php:fpm ставится так же просто:
FROM php:fpm ... RUN pecl install redis && docker-php-ext-enable redis
Как пользоваться
Настраиваем Node.js приложение:
app.use(session({ name: 'sid', secret: 'secret key', cookie: { // Share cookie through sub domains // if you use many domains for service architecture domain : '.your.domain', maxAge : Date.now() + 60000 }, store: new RedisStore({ host : 'redis', port : 6379, client: redis, prefix: 'session:', }) }));
GeekJOBExpressjsSessionHandler::register([ 'name' => 'sid', 'secret' => 'secret key', 'cookie' => [ // Share cookie through sub domains 'domain' => '.your.domain', 'path' => '/', 'maxage' => strtotime('+1hour'), // Set maxage ], 'store' => [ 'handler' => 'redis', 'path' => 'tcp://127.0.0.1:6379', 'prefix' => 'session:', ], // Set to true if signature verification is needed. 'secure' => false ]);
У меня управляемая проверка подписи. Если хотите верифицировать подпись — то выставьте флаг secure=true. Тогда каждая сессионная кука будет проверяться на валидность и регенерироваться, если будет подделана.
Для продакшена рекомендую сконфигурировать часть через php.ini (с учетом что это сервис и он поставляется в Docker, то это логично). В частности мы конфигурируем Redis и немного сессионных параметров:
; After this number of seconds, stored data will be seen as 'garbage' and ; cleaned up by the garbage collection process. ; http://php.net/session.gc-maxlifetime ; default: session.gc_maxlifetime = 1440 ; Redis Sessions use this value for setting TTL session.gc_maxlifetime = maxage - time()
; Lifetime in seconds of cookie or, if 0, until browser is restarted. ; http://php.net/session.cookie-lifetime session.cookie_lifetime = maxage - time()