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

Вы слышали о Стивене Грайдере? Он инженер-программист и фантастически основательный преподаватель многих курсов по React, Redux, структурам данных и многому другому. Всякий раз, когда мне нужно глубоко изучить новую библиотеку или фреймворк, я смотрю, научил ли он ее первым.

Недавно я запустил модуль аутентификации в его Курсе Advanced React и Redux на Udemy в качестве руководства по настройке токенов, которые можно передавать с сервера на интерфейс React / Redux. Почему? Моя команда пытается создать проект именно с такой настройкой. Я взволнованно начал этот курс, только чтобы понять, что он использует MongoDB, а наш проект был привязан к PostgreSQL.

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

Во-первых, вкратце - что такое веб-токен JSON (обычно называемый JWT)? Традиционно пользователь входит в службу, выполняется вызов внутреннего API, и сервер создает файл cookie, который отправляется во внешний интерфейс. Этот файл cookie часто будет содержать зашифрованные данные пользователя, и всякий раз, когда пользователь пытается получить доступ к защищенному ресурсу, учетные данные в файле cookie будут отправлены на сервер в запросе.

Веб-токен JSON похож, но работает немного иначе. Он обычно используется с такими библиотеками, как React, который отображает компоненты в рамках одностраничного приложения (даже если он имитирует обычный веб-сайт с такими инструментами, как React Router). Когда пользователь регистрируется, запрос отправляется на на задней стороне зашифрованный токен создается и отправляется в браузер, и этот токен проверяется при каждом последующем запросе защищенного ресурса. Этот токен не содержит много пользовательских данных - это буквально зашифрованная строка. Все изменения в профиле пользователя происходят на сервере, но токен просто знает, что это действующий пользователь.

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

npm install bcrypt jwt-simple passport passport-jwt passport-local

Начнем с нашей файловой структуры:

Не беспокойтесь о файлах getAlbums и getAlbumById в папке действий - этот шаблон является изменением существующего проекта, и он просто создает данные на странице после успешного входа в систему. Вот беглый взгляд на файл schema.sql, с которым я работаю:

Я собираюсь продолжить работу с нашей моделью. В папке действий, в файле signUp.js, я собираюсь написать и экспортировать запрос, который создаст пользователя в нашей базе данных:

Если вы не беспокоитесь о Webpack на серверной части, просто замените этот оператор импорта на const db = require('../db), предполагая, что вы экспортировали соединение с базой данных под этим именем. Затем давайте завершим запросы, добавив следующие две функции в signIn.js:

Я использовал здесь db.oneOrNone вместо db.one по очень конкретной причине, связанной с тремя параметрами, которые Passport предоставляет вам для аутентификации пользователя в его обратном вызове. Подробнее об этом позже!

Далее я перейду к index.js в нашей папке маршрутов. По сути, это промежуточная станция для всех наших маршрутов, куда мы вставляем специальное промежуточное ПО, которое блокирует неаутентифицированных пользователей:

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

Позвольте мне подробнее рассказать о том, что мы здесь делаем. Passport.js - это библиотека, используемая для различных стратегий аутентификации, таких как OAuth (например, вход в систему с помощью Google или Facebook), JWT и локальная, что по сути означает, что ваш собственный сервер хранит и обслуживает учетные данные аутентификации. В этом файле нам требуется паспорт, файлы служб, которые будут содержать наши стратегии, а затем установить значение requireAuth, равное нашей стратегии JWT, что я подробно объясню. Затем мы вставляем это промежуточное ПО перед защищенным маршрутом - в данном случае это некоторые страницы обзора альбомов.

Перейдем в папку контроллеров и зайдем в authentication.js. Здесь будет создан наш токен, и мы сделаем большую часть работы по регистрации нового пользователя.

Здесь мне нужен jwt-simple, который является пакетом для создания токена. Мне нужен файл конфигурации, который просто экспортирует объект с секретным ключом и строкой… как угодно, черт возьми. Важным моментом здесь является то, что вы помещаете этот файл конфигурации в свой .gitignore и никогда не фиксируете его. Если злоумышленник овладевает им, они потенциально могут расшифровать ваш токен. Я также импортирую функцию запроса createUser из более ранней версии и bcrypt, библиотеку хеширования паролей.

Функция tokenForUser принимает объект пользователя и возвращает закодированный токен, который создается с субъектом (обычно называемым sub), установленным на идентификатор пользователя и временную метку (также обычно называемую iat), вместе с импортированным секретом. Магический алгоритм возвращает очень длинную строку из этой информации. Функция входа внизу просто принимает вошедшего в систему пользователя, вызывает функцию tokenForUser и отправляет этот токен во внешний интерфейс. Более того, он не делает ничего, потому что мы собираемся создать промежуточное программное обеспечение с Passport, которое обрабатывает дешифрование хешированного пароля в относительно ближайшем будущем. Самое интересное в файле происходит в функции signUp:

Что мы здесь делаем? Мы получаем данные пользователя из формы регистрации (или почтальона), которые передаются на сервер в теле запроса. Мы объявляем несколько saltRounds, которые, по сути, решают, на какой мощности будет работать алгоритм хеширования bcrypt. Внимание! Дело не в количестве хешей, а в экспоненциальном увеличении. Раньше стандартным было десять, а теперь рекомендуется двенадцать, но если вы поднимете его слишком высоко, на пробежку может уйти буквально целый день.

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

Теперь мы хэшируем этот пароль! Здесь я использую обещанную версию функции хеширования, потому что по возможности избегаю обратных вызовов. Он принимает пароль пользователя и соль и возвращает обещание, которое преобразуется в строку, которую мы можем передать в нашу createUser функцию вместо обычного текстового пароля. Затем createUser либо преобразуется в блестящий newUser объект, который передается в нашу tokenForUser функцию для генерации и отправки токена, либо у базы данных плохой день, и мы ничего не отправляем обратно, и мы перехватываем эту ошибку. Уф. Давайте экспортируем эти signin и signup функции и продолжим.

Давайте создадим файл authentication.js в нашей папке маршрутов, не так ли? Это будет красиво и просто.

Для переменной Authentication требуются только что написанные нами функции signin и signup. requireSignIn - это промежуточное ПО для паспорта, которое мы вскоре напишем, которое будет обрабатывать запрос входа на сервер. Обратите внимание, что мы устанавливаем для сеанса значение false, потому что JWT не требуют сеансов на сервере. Затем мы вызываем функцию signup, когда пользователь отправляет запрос на почтовый маршрут / sign-up. Маршрут входа / входа немного сложнее - сначала мы собираемся направить пользователя через Passport, и если они пройдут, они перейдут к функции signin, которая передаст им токен.

Мужайтесь, мы почти закончили! Последний большой кусок этой головоломки - это файл password.js в папке services. Он большой, так что я снова разобью его пополам:

Здесь мы собираемся использовать две стратегии Passport: одна - это стратегия JWT, а другая - локальная. Локальная стратегия будет регистрировать нашего пользователя на нашем сервере. Passport ожидает конкретной информации об именах пользователей и паролях, поэтому, если переменная, передаваемая из формы, отличается от username, нам нужно явно сообщить об этом Passport. Я использую адрес электронной почты в качестве имени пользователя, поэтому передал его. Однако, поскольку я использую password в качестве пароля, мне не нужно их сообщать.

localLogin является экземпляром класса LocalStrategy Passport. Ему передается объект localOptions, который мы определили, и функция обратного вызова с информацией о пользователе и done, которые вы увидите повсюду в Passport и, как правило, в асинхронном режиме. Здесь я передаю адрес электронной почты пользователя в verifyUser запрос к базе данных, созданный ранее, и, если пользователь существует, я сравниваю пароль, введенный пользователем в форму, с хешированным паролем, возвращенным для этого пользователя из базы данных с помощью функции .compare bcrypt. Это возвращает обещание, которое разрешается в логическое значение. Если это правда, мы возвращаем done функцию Passport с null и validUser. Передача этого пользовательского объекта очень важна, потому что помните эту строку в routes/authentication.js:

router.post('/sign-in', requireSignIn, Authentication.signin)

Функция Authentication.signin требует, чтобы этот пользователь создал токен.

Теперь о стратегии JWT Passport:

Во-первых, это объект параметров JWT. jwtFromRequest использует метод ExtractJwt из пакета jwt-simple, чтобы сообщить серверу, где искать JWT. В этом случае он будет размещен в заголовке под authorization. Эта функция по существу декодирует зашифрованный JWT и позволяет нам извлечь любой установленный нами идентификатор пользователя, чтобы сравнить его с реальным пользователем в базе данных. Ключ secretOrKey - это строка случайной ерунды, которую мы ранее поместили в секретный объект в нашем файле конфигурации.

jwtLogin немного сложнее понять. Мы внедряем новую стратегию Passport, разработанную специально для JWT. Мы передаем ему только что созданный jwtOptions со словами: «Вот - вот как вы декодируете токен». Второй аргумент - это функция обратного вызова с параметром полезной нагрузки и done. Payload - это незашифрованные данные токена. Вы помните, как мы создали этот токен с помощью объекта? (Хорошо, прошло какое-то время, и я много кидал в вас, так что давайте просто поделимся этим еще раз ...):

const tokenForUser = (user) => {
  const timestamp = new Date().getTime()
  return jwt.encode({sub: user.id, iat: timestamp}, config.secret)
}

Таким образом, payload.sub эквивалентен user.id. Если бы мы открыли console.log payload.iat, то увидели бы время создания токена на распечатке.

returnUserById - это запрос к базе данных, который выполняет то, что, по его словам, делает. После того, как обещание выполнено и предполагается, что пользователь успешно получен, мы передаем этого пользователя в Passport и отправляем его веселым путем к защищенным ресурсам!

Хорошо, давайте проверим все это. Я собираюсь перейти в Почтальон и зарегистрировать нового пользователя.

Успех! Теперь я собираюсь изменить маршрут на / sign-in и просто ввести адрес электронной почты и пароль, и созерцать красоту:

Я очень, очень надеюсь, что это будет полезно всем, кто пытается взяться за эту задачу. Как всегда приветствуется конструктивная критика и высокие пятерки!