Прежде чем вы это скажете, я знаю, я знаю. Уже существует буквально тонна руководств MERN, показывающих, как использовать JWT (веб-токены JSON) и Passport.js с Express.

Но вот то, что не упоминалось в каждом из этих руководств, и что я расскажу:

КАК и ПОЧЕМУ использовать различные варианты аутентификации, предлагаемые Passport (в том числе passport-jwt), и подводные камни, которые меня сбили с толку

часами напролет, пока я составлял заявку на регистрацию пользователя.

Я не знаю, каков был ваш опыт, но большинство проектов, над которыми я работал профессионально, прошли с авторизацией, уже созданной кем-то другим. Так что, помимо улучшения моих навыков MERN, я рассматривал это как возможность узнать больше о безопасности и аутентификации и различных способах этого.

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

Но прежде чем я перейду ко всему этому, позвольте мне вкратце познакомиться с аутентификацией JWT и Passport.js.

Что такое веб-токен JSON?

Веб-токен JSON, согласно сайту, это:

… Открытый стандарт (« RFC 7519 ), который определяет компактный и автономный способ безопасной передачи информации между сторонами в виде объекта JSON». - JWT.io

По сути, JWT - это токены с цифровой подписью, которые можно проверять и доверять, и они становятся все более и более популярными для обеспечения безопасности и авторизации между сторонами (например, серверами и клиентами) и для обмена конфиденциальной информацией, при этом проверяя, что токен не был подделан. с или в декодированном виде.

Я не буду вдаваться в подробности того, как работают токены, вы можете прочитать все об этом из гораздо более квалифицированного источника, чем я здесь, но достаточно сказать, что я решил использовать JWT как часть моей стратегии проверки пользователей. вместе с Passport.js, чтобы сделать мое простое приложение для регистрации пользователей безопасным (и потому, что мне было любопытно посмотреть, смогу ли я это сделать, поскольку у меня не было большого опыта реализации авторизации).

После этого следующая часть моего решения для аутентификации - Passport.js.

Что такое Passport.js и зачем его использовать?

Паспорт:

Простая ненавязчивая аутентификация для Node.js - Passport.js

и он очень хорошо работает с Express. Passport - это промежуточное программное обеспечение аутентификации для Node, которое служит одной цели - аутентификации запросов по модульному принципу, оставляя все остальные функции самому приложению, делая код более чистым, более простым в обслуживании и обеспечивая четкое разделение проблем.

Если вы введете термин «промежуточное ПО для аутентификации JavaScript» или даже просто «аутентификация JavaScript» в строке поиска Google, Passport.js попадет в пятерку лучших результатов поиска. Вот насколько повсеместно это решение в экосистеме JavaScript.

Я уже упоминал, что он может похвастаться более чем 500+ стратегиями аутентификации? Оно делает. Если вы хотите войти в систему с простым именем пользователя и паролем, с учетными данными Github, Facebook, oAuth и т. Д., Вероятно, для этого есть стратегия Passport.js.

Так что выбрать Паспорт как часть моей стратегии авторизации было простым решением.

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

Как реализовать их в Express.js (и немного в React)?

А теперь самое интересное: как реализовать Passport.js и JWT в приложении Express / Node? Если честно, это на какое-то время поставило меня в тупик. Но после многочисленных руководств, перечитывания документации и обращения за помощью к Stack Overflow я пришел к пониманию и удовлетворению.

Я должен сказать, что одной из моих целей для этого приложения было сделать его модульным, упростить добавление или удаление функций, поэтому, когда вы увидите мою файловую структуру, вы заметите все маршруты для функциональности CRUD (создать, прочитать , обновление, удаление) встроены в отдельные файлы, и вся аутентификация по паспорту также обрабатывается в одном централизованном файле. Взгляните на файловую структуру API-части моего приложения.

Дерево файлов API

root/
├── api/ 
| ├── config 
| | ├── passport.js 
| | ├── jwtConfig.js
| ├── server.js 
| ├── sequelize.js 
| ├── package.json
| ├── models/ 
| | ├── user.js
| ├── routes/ 
| | ├── deleteUser.js 
| | ├── findUsers.js 
| | ├── loginUser.js 
| | ├── registerUser.js
| | ├── updateUser.js
| ├── node-modules/

Файл package.json

А вот package.json и его зависимости, чтобы вы могли точно увидеть, что я использую.

Зависимости включают в себя несколько дополнительных функций, таких как babel, поэтому я могу использовать синтаксис ES6 в моем приложении Node.js, bcrypt для хеширования паролей и sequelize в качестве ORM MySQL, но вам нужно сосредоточиться на jsonwebtoken, passport, passport-local и passport-jwt. Это самое необходимое для этого блога.

Файл server.js

Сначала я начну с файла server.js, поскольку он требует минимум пояснений. Этот файл предназначен исключительно для запуска сервера, инициализации использования Passport в приложении и настройки маршрутов и анализа запросов со стороны клиента.

Довольно просто, правда? Большой. Двигаемся дальше.

Вы также могли заметить require('./config/passport');, это следующий шаг, который я перейду. Конфигурация JWT и паспорта внутри папки с именем config.

Файл jwtConfig.js

Конфигурация JWT очень проста, это секрет, необходимый JWT для кодирования и декодирования токенов. Обычно это сохраняется как переменная окружения в файле, который не зарегистрирован в Github, но, чтобы показать, как это работает, я установил его здесь.

Файл passport.js

Настоящая настройка для всей моей аутентификации по паспорту находится в файле passport.js. Вот.

Я понимаю, что это большой объем кода, который нужно принять. Если вы хотите увидеть только код для Passport и маршруты с аутентификацией Passport, я создал несколько сущностей здесь с кодом.

Я рассмотрю два типа реализации паспорта, происходящие здесь: паспорт-локальный для методов register и login и паспорт-jwt для метода jwt.

Локальный паспорт использует имя пользователя и пароль, а паспорт-jwt использует полезную нагрузку JWT для проверки подлинности пользователя.

Как я уже сказал ранее, документация для обоих, использующих только Express.js, довольно хороша, но хитрость заключается в объединении ее с базой данных MySQL и информацией, передаваемой от клиента React во внешнем интерфейсе.

После того, как в стратегию локального паспорта были переданы имя пользователя и пароль (которые я проверяю, что оба ввода заполнены, по крайней мере, до того, как я когда-либо обращаюсь к серверу), первая проверка, которую я выполняю, - это Sequelize, мой SQL ORM (например, Mongoose для MongoDB ), чтобы определить, существует ли это имя пользователя в базе данных. Если он возвращает null, аутентификация не выполняется (ни один пользователь в базе данных не соответствует), и обычно 401 Unauthorized будет возвращаться с сервера клиенту без дополнительной информации.

Ошибка с паспортом №1: информация в обработке ошибок

Этот тип обработки ошибок был для меня самым разочаровывающим в Passport. Отсутствие информации, позволяющей пользователю (или мне) узнать, какая фактическая ошибка была за пределами 401 Unathorized.

Но есть лучшее решение, требующее небольшой дополнительной работы, с настраиваемыми обратными вызовами. Я скоро расскажу более подробно об обратном вызове, но пока вы можете увидеть, есть ли какая-то ошибка, вместо того, чтобы возвращать return done(null, user);, паспорт может передать обратно return done(null, false, { message: 'bad username or passwords don't match' });. Затем это сообщение может быть передано обратно с сервера клиенту, фактически сообщая пользователю, в чем проблема (это то, что я хотел для этого приложения). Если вы предпочитаете просто сообщить пользователю, что его аутентификация не удалась, но не раскрывать причину, это тоже круто. Ваш выбор, но я хочу показать, как можно отправлять сообщения об ошибках.

Остальная часть стратегии локального паспорта довольно очевидна: на register пароль пользователя хешируется и обрабатывается пакетом шифрования bcrypt, а затем, когда вызывается метод login, он хеширует вновь введенный пароль и проверяет пароли с помощью bcrypt. compare перед возвратом положительного или отрицательного результата проверки.

Теперь это подводит меня к стратегии паспорта-jwt, которая называется jwt. Это аутентификация, которая вызывается на защищенных маршрутах в приложении: findUsers, updateUser, deleteUser. Для стратегии jwt JWT передается обратно от клиента при каждом вызове сервера (я передал свой в заголовке авторизации с ключом: JWT), который извлекается и затем декодируется с использованием секрета (который хранится в другом файле , но на самом деле это должна быть переменная среды, известная только системе).

После того, как полезная нагрузка JWT расшифрована, идентификатор (который для меня является именем пользователя) может быть найден в базе данных так же, как это делает стратегия локального паспорта, и возвращен либо с done(null, user);, если пользователь найден, либо с done(null, false);, если это не так. (чего почти никогда не должно происходить, потому что JWT включает имя пользователя в зашифрованной форме, поэтому, если это каким-то образом не было изменено или база данных не была, он должен быть в состоянии найти пользователя).

Попался паспорт # 2: паспорт - местный житель хочет вернуть, паспорт - JWT - нет

Это подводит меня ко второй проблеме, которая сбила меня с толку на долгое время; не все паспортные стратегии требуют одинакового разрешения. Если вы присмотритесь, то увидите, что в обеих стратегиях «паспорт» есть return done(null, user);, а в стратегии «паспорт-jwt» есть done(null, user);. Увидеть разницу? Это незначительно, но наличие (или удаление этого return) - это разница между передачей пользовательских данных обратно из промежуточного программного обеспечения на сервер или нет.

И это остановило мой прогресс на несколько часов, прежде чем доброта Stack Overflow помогла мне разобраться с проблемой (это первый раз, когда мне действительно приходилось спрашивать у SO ответ, который я уже не мог найти, но это было хорошо стоило того). Так что знайте, когда return или нет.

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

Файл registerUser.js

Следующим шагом будет реализация этих новых методов внутри различных маршрутов. Вот маршрут registerUser.

Как видите, эта реализация паспорта выглядит немного иначе, чем большинство примеров, и это потому, что я использую версию пользовательского обратного вызова Passport, для которой требуется закрывающий вызов. Я использую это для того, чтобы сообщение об ошибке, которое возвращается в случае сбоя аутентификации Passport, можно было отправить с сервера клиенту вместо непонятного 401 Unauthorized.

Поскольку это реализуется как обратный вызов (вот почему вы видите (req, res, next) не один раз, а дважды в этих сценариях; это дает мне доступ к (err, user, info) из промежуточного программного обеспечения Passport. info - это то, что меня интересует - это сообщение отправлено обратно, поэтому, если информация отличается от нуля, это означает, что аутентификация не удалась, и я могу отправить сообщение клиенту, чтобы сообщить ему, почему.

Уловка с паспортом №3: не забывайте `req.logIn ()` в пользовательском обратном вызове

Теперь я перехожу к третьей проблеме с Passport: для того, чтобы замыкания и пользовательские обратные вызовы работали с паспортным локальным, необходимо вызвать небольшой метод req.logIn() до обработки user данных, если они успешно возвращаются из промежуточного программного обеспечения.

Это задокументировано в документации по паспорту, но его важность не подчеркивается так сильно, как хотелось бы (и я пропустил это в первые несколько раз, пытаясь заставить работать пользовательские обратные вызовы), поэтому я выделю это сейчас.

Это должно произойти, и как только это будет сделано, я могу получить дополнительные данные со стороны клиента для регистрации пользователя, найти того же пользователя, созданного в базе данных во время локального вызова register для паспорта, и обновить файл пользователя с помощью дополнительная информация. Я мог бы передать эти дополнительные данные и промежуточному программному обеспечению, но я хочу, чтобы Passport обрабатывал только аутентификацию, а не создание пользователей. Помните, модуляризация.

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

После того, как все это было успешно выполнено, клиенту отправляется сообщение о состоянии HTTP 200 и успешное выполнение.

Файл loginUser.js

Теперь увидеть путь loginUser должно иметь больше смысла.

Используется тот же стиль пользовательских обратных вызовов и закрытий, самая большая разница в том, что после успешной проверки пользователя и его размещения в базе данных токен JWT генерируется с помощью функции jwt.sign();, которая устанавливает имя пользователя как идентификатор, переданный в JWT, и зашифрован секретом, который я установил ранее.

Еще раз, если все это работает успешно, отправляется HTTP-статус 200 с логическим значением, которое я назвал auth, установленным в true для клиентской стороны, вновь сгенерированным токеном и коротким сообщением об успешном входе в систему.

Файл findUsers.js

И наконец, что не менее важно, это маршрут findUsers. Я использовал ту же аутентификацию паспорт-jwt для маршрутов updateUser и deleteUser, поэтому я просто покажу это в качестве примера.

Я использовал пользовательские обратные вызовы для всех маршрутов с аутентификацией Passport (я ценю хорошую и четкую обработку ошибок), поэтому этот стиль кода к настоящему времени должен выглядеть рутинным.

На этот раз, когда вызывается passport.authenticate(), я реализую стратегию JWT, определенную в файле passport.js. То же (err, user, info) возвращается от промежуточного программного обеспечения, но на этот раз от req.logIn() не поступает вызов. Вместо этого я просто возвращаю объект user, найденный во время аутентификации, и передаю клиенту все необходимые поля вместе с логическим значением auth и сообщением об успешном завершении. Токен JWT по-прежнему хранится в локальном хранилище на стороне клиента, поэтому нет необходимости повторно создавать или передавать его клиенту. Это подводит меня к заключительному этапу получения паспорта.

Ошибка с паспортом №4: правильная передача заголовков авторизации

Это не совсем то, что связано с паспортом, но это еще одна вещь, которая меня сбила с толку.

Как я уже сказал, я собрал воедино свое (теперь гораздо более полное) понимание JWT и Passport из множества других руководств и документации, и одно из этих руководств сбило меня с пути. Это заставило меня передать JWT обратно клиенту в чем-то помимо настоящих заголовков авторизации, что является способом, который я выбрал, чтобы моя стратегия паспорта-jwt извлекала полезную нагрузку JWT из запроса клиента. По этой причине я хотел кратко коснуться одного фрагмента кода внешнего интерфейса, где я передаю токен в правильном формате, чтобы вам не пришлось тратить время на отладку, как это сделал я.

Я использую популярный HTTP-клиент Axios, основанный на обещаниях, для выполнения вызовов к серверу, но вам действительно стоит обратить внимание на раздел headers. Перед вызовом сервера я извлекаю JWT из локального хранилища, используя localStorage.getItem('JWT') (ключ, который я установил со значением JWT, когда я передал токен вперед с маршрута loginUser() на стороне сервера), а затем я установил его с помощью headers: { Authorization: `JWT ${accessString}` } и небольшая интерполяция строки ES6. Таким образом, если бы мои заголовки авторизации содержали более одного значения для синтаксического анализа, паспорт-jwt все равно мог бы легко извлечь правильную информацию о полезной нагрузке JWT, найдя строку, связанную с JWT.

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

Заключение

Я знаю, что много написал (и предоставил кучу фрагментов кода и смысла), но аутентификация - это непростая идея. Также невозможно сделать приложение настолько безопасным, что никто никогда не сможет его взломать. Но если вы примете меры предосторожности, такие как промежуточное ПО для аутентификации JWT и Passport.js, ваше приложение Node.js должно иметь больше шансов противостоять злоумышленникам.

Обязательно обратите внимание на ошибки, которые я выделил для Passport - и вы попадете на более безопасный сайт.

Спасибо за чтение, я надеюсь, что это окажется полезным и даст вам лучшее понимание реализации аутентификации Passport и использования веб-токенов JSON. Мы очень ценим аплодисменты и акции!

Если вам понравилось это читать, возможно, вам понравятся и другие мои блоги:

Ссылки и дополнительные ресурсы: