Сегодня я расскажу историю одного небольшого проекта, причину создания которого можно описать одним словом «психанул». Нет, на деле, конечно, я не психанул, всего лишь был немного раздосадован тем, как обстоят дела с сессиями в Node.js и тем, как все поголовно используют Passport, который с одной стороны берёт на себя неплохой объём работы, а с другой переусложнён и обладает устаревшим и запутанным API.

Проблематика

Началось всё довольно давно, с моего знакомства с Node.js в 2014-м. Тогда я поддерживал RESTful API, написанный на фреймворке Express, сессии в котором были реализованы с помощью Passport. Это вряд ли можно было назвать правильными инструментами для тех задач, но тогда я впервые столкнулся с избыточностью Passport и недостаточной гибкостью его сессионных миддлвар. В моём проекте требовалось передавать идентификаторы сессий через кастомный HTTP-заголовок, а не через cookie. Чтобы добиться этого, я делал несколько подходов, пытался законтрибьютить, но в итоге всё закончилось банальным форком, в котором была реализована нужная мне функциональность.

Последняя капля

Время шло, на дворе был уже 2017-й год, я занимался реализацией одного API для своего домашнего проекта на том же Node.js (в этот раз у меня на руках уже был фреймворк Restify). Но, так вышло, что индустриальный стандарт в виде Passport добрался и до туда. Нет, на самом деле, не очень-то он и добирался, просто так вышло, что API его миддлвары подходил к в том числе и к Restify. И, в какой-то прекрасный момент, отлаживая некоторую странность в поведении авторизации, со мной и случилось то самое «психанул». Я увяз в коллбэках, и меня вконец достал неявный нейминг абстракций Passport, который был сделан, мягко говоря, совсем не для людей.

Какое-то время поискав альтернативы Passport’у, я понял, что ничего хорошего в обозримом пространстве нет. В голове пронеслась мысль, многим наверное уже набившая оскомину: «если хочешь сделать что-то хорошо, сделай это сам».

Тогда-то, засучив рукава, я сел и начал писать свой первый серьёзный проект с открытым исходным кодом, который и получил имя auzy, как вы могли уже догадаться.

Проект и его концепция

Насмотревшись и натерпевшись от старых подходов, я твёрдо решил, что у проекта будет современная кодовая база с поддержкой последней стабильной версии Node.js, на тот момент 8-й. Благо, у меня уже был неплохой опыт работы с промисами, async/await, классами и многими другими прелестями ES6+.

Вторым важным моментом должно было стать достаточное покрытие тестами. Согласитесь, зачастую проект без тестов выглядит как-то несерьёзно, да и вообще, я бы опасался использовать такой проект в продакшен среде. Здесь я решил писать как юнит-тесты, так и функциональные, взяв современный фреймворк Jest.

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

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

Разработка

За все свои годы работы с вебом, мне не раз доводилось иметь дело с сессионной логикой. Несколько раз приходилось реализовывать её с нуля. Для меня в этой логике не было ничего особенного, в голове она выстраивалась как на ладони, а пережитый опыт лишь добавлял уверенности и подливал масло в огонь.

Вечерами около месяца я активно работал над проектом, и он наконец начал обретать желаемый вид. Мне удалось реализовать поддержку Restify, Connect, Express и koa.js, покрыть проект функциональными и юнит-тестами. Иными словами, минимальный результат был достигнут.

В конце концов, настал момент истины, и захотелось понять, а какой же проект вышел лучше. Нет, конечно же я понимал, что auzy не решал всех тех задач, которые решал Passport, но и наоборот, auzy с лёгкостью делал то, что Passport делал с большим скрипом. И вышло так, что некоторое пересечение у них есть, а значит на этом пересечении есть и возможность продемонстрировать, каким же вышел auzy.

Подключение в проект

Несколько слов о технологическом стеке проекта, которому требовались сессии. Фреймворк Restify, база данных MongoDB, хранилище сессий Redis. Идентификатор сессии требовалось передавать в заголовке (к слову, auzy пока не поддерживает передачу сессий в куках). В данном случае express-session-header это форк, как раз позволяющий передавать сессию в заголовках.

Passport

Давайте взглянем, как выглядело раньше подключение Passport в проекте:

const passport = require('passport');
const session = require('express-session-header');
const RedisStore = require('connect-redis')(session);
const redisStore = new RedisStore({
    ttl: 60 * 60 * 24 * 30 * 6,
});
server.use(session({
    secret: config.session.cookie.secret,
    store: redisStore,
    resave: false,
    saveUninitialized: false,
    header: 'X-Session-Token',
}));
server.use(passport.initialize());
server.use(passport.session());
const LocalStrategy = require('passport-local').Strategy;
passport.use(new LocalStrategy({
        usernameField: 'user[email]',
        passwordField: 'user[password]',
    },
    function (email, password, done) {
        // логика поиска пользователя в БД и возврат через done
    },
));

passport.serializeUser(function (user, done) {
    return done(null, user.id);
});

passport.deserializeUser(function (id, done) {
    User.Model.findOne({_id: new ObjectId(id), status: User.statuses.ACTIVE}, done);
});

Громоздко, не правда ли? Что здесь у нас? require 4-х модулей, 3 миддлвары через use, логика serialize и deserialize. Ну а локальная стратегия (LocalStrategy) неподготовленному человеку вообще покажется лёгким безумством, это объект, который в свою очередь является миддлваром для Passport, у которого ещё и формат полей задан в виде user[email], хотя на деле это не обязательно urlencoded, это также может быть путём в объекте.

Кому как, а на мой взгляд всё это выглядит чрезмерно запутанным. Я тратил немало нервов, когда доходил до этих кусков кода, особенно когда нужно было что-то поменять. С auzy от этих ощущений не должно было остаться и следа.

auzy

Итак, когда я начал подключать auzy к своему домашнему проекту, это для меня стало глотком свежего воздуха:

const auzy = require('auzy');
const config = {
    session: {
        sessionName: 'X-Session-Token',
        ttl: 60 * 60 * 24 * 30 * 6 * 1000,
        alwaysSend: true,
        loadUser: sessionData => {
            // логика поиска пользователя в БД и возврат через промис
        },
    },
};
const environment = {
    framework: 'restify',
    storage: 'redis',
    transport: 'header',
};
server.use(auzy(config, environment));

Благодаря тому, что auzy знает о популярных фреймворках, подключение выглядит заметно проще, оно занимает примерно в 2 раза меньше строк, где выполняется один require и use всего 1-го миддлвара. И это всё при нулевых зависимостях (в будущем, возможно, добавится библиотека cookie, но пока ноль).

Что же с API? Если кратко, то почти всё задаётся через конфигурацию. Здесь стоит выделить основные сущности, которыми оперирует auzy:

  • framework специфицирует, каким образом будет выполняться работа с объектами запросов/ответов, как будет складываться и удаляться из них пользователь.
  • storage определяет хранилище данных для сессий.
  • transport реализует способы передачи идентификаторов сессий, например, в заголовке.

Десериализация и локальная стратегия Passport заменены на функцию loadUser, а от сериализации и аутентификации осталась только аутентификация, которая выставляет в сессию данные настоящего пользователя. В данном случае под аутентификацией я конечно имею ввиду лишь последний этап фиксации успешности аутентификации.

Планы

Конечно, есть ещё много всего, что было бы неплохо сделать в проекте. Но тут как с ремонтом, его нельзя закончить, его можно только прекратить. И пока я переключился обратно на свой домашний проект, у auzy томится TODO-список. Надеюсь, в скором будущем у меня найдётся время продолжить разработку.

На этом я завершаю свое немалое повествование о том, как я двинулся в СПО. Спасибо всем, кто дочитал до этих строк.

Ссылки

  1. Само собой, главный герой заметки auzy.
  2. Если вам нужна особая гибкость, пока ещё имеет смысл взглянуть на Passport.