Авторизация в системах одна из ключевых частей. Можно использовать какие то мощные решения, Firebase например, или что то из множества хороших библиотек (имеются в виду для node.js). Если хочется уменьшить количество зависимостей или для самообразования - то можно написать свое.
Вот так через несколько итераций, со временем, выработалось решение, которое построено с использованием Nginx. Все описанное является очень частным случаем используемого подхода, в том смысле что есть некоторые условия в которых требовалось создать решение, и данное решение хорошо только в в этих условиях.
В замечательном Nginx (вероятно не только в нем) есть простая и очень эффективная возможность (auth_request) конфигурировать выполнение подзапросов на каждый запрос. Иными словами при обращении клиентов к вашему API который принимает подключения на порту (например) 30000, Nginx выполнит auth_request - авторизационный подзапрос на определяемый вами порт (например) 25000. Запускаем приложение которое слушает 25000 порт, обрабатывает запрос и возвращает данные содержащие все необходимое для аутентификации в API инициатора запроса (клиента). Эта часть общеизвестна и с более подробной информацией проблем быть не должно.
Для реализации подобной схемы с помощью node.js приложений можно использовать любой модуль реализующий http сервер, даже встроенный в node.js модуль http прекрасно подойдет с минимальным написанием кода. В примере используется собственный модуль просто для удобства. Также я не буду заострять внимание на настройке Nginx - в данном случае это не важно, в крайнем случае по ссылке приведенной выше есть примеры конфига и описание директив.
Описание
Сама обработка внутри AUTH - это последовательность методов, принимают на вход опции (о них ниже), результат вызова предыдущего, объекты модуля http : запроса (request) и ответа (response). Самый первый метод в качестве данных получает тоже request
Ссылка на код в конце статьи приведена.
После запуска и чтения конфигураций (./configs/.), идет построение для каждого пути (end-point) своей последовательности обработки из указанных методов и опций, передаваемых им. За подробностями можно посмотреть app.js метод BuildWorkers.
Все варианты методов - обработчиков расположены по пути ./workers/ и крайне просты. Например, конфигурационный файл ./configs/api.config.js
Первый метод reqprops с параметрами properties:['headers','apikey'] будет пытаться прочитать значение в свойствах с указанными именами, а именно - (на вход поступит request) request.headers, у полученного значения ищет заголовок с именем apikey. Если значение найдено - оно возвращается из метода и будет передано в следующий метод, согласно конфигурации - req. В свою очередь метод req получив первым аргументом options из конфига, выполнит запрос по указанному пути и порту передав второй аргумент (входные данные). В данном случае это и есть декодирование jwt. Полученные данные от внешнего сервиса декодирования jwt, модульвернет для обработки последующими модулями.
{
path: "/api/*",
workers:[
{
name:"reqprops",options:{properties:['headers','apikey']}
},
{
name:"req",
options:{port:jwt.port,path:jwt.decode_path,data:{secret: jwt_secret} }
},
{
name:"inlist",
options:{field:"id",list:["apidev"]}
},
{
name:"reqprops",options:{properties:['body']}
}
]
}
Дальше вызовется метод проверки наличие значения (в данном случае от jwt вернется объект с идентификатором в поле id) в списке разрешенных. Не обязательно держать в конфигурации список, здесь может быть такой же внешний запрос к другому модулю или к БД. И последним методом в цепочке стоит все тот же reqprops но уже с другим именем свойства: body которое будет прочитано и вернется. Поскольку дальше методов в конфигурации нет, это значение будет передано в следующий обработчик самого http сервера по этому пути - но это уже совсем другой модуль.
Поскольку в самом jwt получаемом от клиента хранится только ключ данных, то эти данные должны где то быть - за это отвечает класс Data (./data.js) И два метода: для чтения данных и записи. Внешний приложения могут в любой момент прочитать или записать эти данные через api приложения (./api/). В методах - воркерах есть метод getdata который и читает данные из Data передавая их дальше, и если следом поставить метод setdata - то эти самые данные и будут отправлены в nginx в ответ на запрос auth_request. Дальше уже в nginx эти данные устанавливаются в заголовке и читаются в конечном приложении обрабатывающем основной запрос.
Можно довольно много вариантов последовательностей задать исключительно через конфиги, но даже если будет этих воркеров недостаточно - легко добавить свой собственный. Вероятно редактирование конфигов через какую то визуальную реализацию было бы еще проще и удобнее.
Вот так через несколько итераций, со временем, выработалось решение, которое построено с использованием Nginx. Все описанное является очень частным случаем используемого подхода, в том смысле что есть некоторые условия в которых требовалось создать решение, и данное решение хорошо только в в этих условиях.
В замечательном Nginx (вероятно не только в нем) есть простая и очень эффективная возможность (auth_request) конфигурировать выполнение подзапросов на каждый запрос. Иными словами при обращении клиентов к вашему API который принимает подключения на порту (например) 30000, Nginx выполнит auth_request - авторизационный подзапрос на определяемый вами порт (например) 25000. Запускаем приложение которое слушает 25000 порт, обрабатывает запрос и возвращает данные содержащие все необходимое для аутентификации в API инициатора запроса (клиента). Эта часть общеизвестна и с более подробной информацией проблем быть не должно.
Для реализации подобной схемы с помощью node.js приложений можно использовать любой модуль реализующий http сервер, даже встроенный в node.js модуль http прекрасно подойдет с минимальным написанием кода. В примере используется собственный модуль просто для удобства. Также я не буду заострять внимание на настройке Nginx - в данном случае это не важно, в крайнем случае по ссылке приведенной выше есть примеры конфига и описание директив.
- Nginx получает запрос к /api
- Nginx выполняет подзапрос к /auth
- Auth берет данные из запроса и выполняет ряд действий в т.ч. (не обязательно) с использованием внешних модулей (например декодирование токена jwt полученного в cookie запроса )
- Внешний модуль возвращает данные в Auth
- Auth возвращает ответ в Nginx который полученные данные прикрепляет дополнительно к основному запросу ( auth_request_set)
- Nginx выполняет первоначальный запрос к API
- API читает дополнительные данные и по ним аутентифицирует запрос, возвращает ответ
Сама обработка внутри AUTH - это последовательность методов, принимают на вход опции (о них ниже), результат вызова предыдущего, объекты модуля http : запроса (request) и ответа (response). Самый первый метод в качестве данных получает тоже request
Ссылка на код в конце статьи приведена.
После запуска и чтения конфигураций (./configs/.), идет построение для каждого пути (end-point) своей последовательности обработки из указанных методов и опций, передаваемых им. За подробностями можно посмотреть app.js метод BuildWorkers.
Все варианты методов - обработчиков расположены по пути ./workers/ и крайне просты. Например, конфигурационный файл ./configs/api.config.js
Первый метод reqprops с параметрами properties:['headers','apikey'] будет пытаться прочитать значение в свойствах с указанными именами, а именно - (на вход поступит request) request.headers, у полученного значения ищет заголовок с именем apikey. Если значение найдено - оно возвращается из метода и будет передано в следующий метод, согласно конфигурации - req. В свою очередь метод req получив первым аргументом options из конфига, выполнит запрос по указанному пути и порту передав второй аргумент (входные данные). В данном случае это и есть декодирование jwt. Полученные данные от внешнего сервиса декодирования jwt, модульвернет для обработки последующими модулями.
{
path: "/api/*",
workers:[
{
name:"reqprops",options:{properties:['headers','apikey']}
},
{
name:"req",
options:{port:jwt.port,path:jwt.decode_path,data:{secret: jwt_secret} }
},
{
name:"inlist",
options:{field:"id",list:["apidev"]}
},
{
name:"reqprops",options:{properties:['body']}
}
]
}
Дальше вызовется метод проверки наличие значения (в данном случае от jwt вернется объект с идентификатором в поле id) в списке разрешенных. Не обязательно держать в конфигурации список, здесь может быть такой же внешний запрос к другому модулю или к БД. И последним методом в цепочке стоит все тот же reqprops но уже с другим именем свойства: body которое будет прочитано и вернется. Поскольку дальше методов в конфигурации нет, это значение будет передано в следующий обработчик самого http сервера по этому пути - но это уже совсем другой модуль.
Поскольку в самом jwt получаемом от клиента хранится только ключ данных, то эти данные должны где то быть - за это отвечает класс Data (./data.js) И два метода: для чтения данных и записи. Внешний приложения могут в любой момент прочитать или записать эти данные через api приложения (./api/). В методах - воркерах есть метод getdata который и читает данные из Data передавая их дальше, и если следом поставить метод setdata - то эти самые данные и будут отправлены в nginx в ответ на запрос auth_request. Дальше уже в nginx эти данные устанавливаются в заголовке и читаются в конечном приложении обрабатывающем основной запрос.
Можно довольно много вариантов последовательностей задать исключительно через конфиги, но даже если будет этих воркеров недостаточно - легко добавить свой собственный. Вероятно редактирование конфигов через какую то визуальную реализацию было бы еще проще и удобнее.
Спасибо что дочитали, надеюсь подход/решение окажется кому то полезным.
Nginx + Node.js: делаем идентификацию и аутентификацию
Авторизация в системах одна из ключевых частей. Можно использовать какие то мощные решения, Firebase например, или что то из множества хороших библиотек (имеются в виду для node.js). Если хочется...
habr.com