Пошаговая и подробная реализация

Строительные блоки — краткое введение

Когда дело доходит до аутентификации приложений (например, аутентификации пользователей), аутентификация на основе веб-токенов (JWT) JSON является одним из наиболее распространенных вариантов, среди других вариантов, таких как файлы cookie браузера и аутентификация сеанса, OAuth/ OAuth2 и OpenID Connect.

Любой может внедрить JWT в любой проект. Например, NestJS предлагает нам модуль solo @nestjs/jwt, который позволяет нам интегрировать JWT в наш проект. Тем не менее, это требует много шаблонов. Таким образом, предпочтительным решением может быть использование внешнего инструмента/пакета, специализирующегося на аутентификации.

Пакет Passport представляет собой широко используемую библиотеку NodeJS, единственной целью которой является аутентификация запросов, которую он выполняет с помощью расширяемого набора плагинов, известных как стратегии. На момент написания этого поста в арсенале Паспорта было порядка 537 «стратегий! В частности, для аутентификации на основе JWT Passport, конечно же, предлагает нам соответствующую стратегию JWT.

Поскольку Passport — самая популярная библиотека аутентификации NodeJS, NestJS предлагает собственный модуль @nestjs/passport, который позволяет напрямую интегрировать Passport в любое приложение NestJS.

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

Документация NestJS содержит очень похожий пример, и предполагается, что вы уже предприняли некоторые из описанных там шагов. Однако здесь мы пройдем через 2 шага.

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

Базовый репозиторий

Для вашего удобства (учитывая ваше время) вы можете использовать пример Мой репозиторий GitHub. Ключевые моменты репозитория резюмируются следующими пунктами:

  • Репозиторий (как и в Документации NestJS) уже включает как AuthModule, так и UsersModule соответственно, а также их классы Controller и Service.
  • Вместо использования UsersService для основных операций CRUD репозиторий использует отдельный класс solo @injectable, DbRepo. Он обслуживает как AuthService, так и UsersService. [Вы можете прочитать, как использовать класс solo @injectable в качестве singleton Provider в другом моем посте]
  • DbRepo получает некоторых пользователей в массиве, которые были предварительно установлены в классе UserDB.
  • Установлены пакеты class-validator и class-transformer. DTO и объекты определены в папке src/dataObjects и оформлены соответствующим образом. [узнать больше в посте]
  • В репозитории используются внешние переменные, поэтому пакет @nestjs/config уже установлен и объявлен глобально в AppModule. Внешние переменные определены в файле src/config/.env.dev. [подробнее в моем посте]
  • Кроме того, в репозитории используется инструмент проверки схемы Joi (который также используется в Документации NestJS для проверки схемы объекта). Кроме того, в файле src/config/config.schema.ts был определен и декорирован (с использованием соответствующих декораторов свойств Joi) валидатор схемы объекта JSON. Он используется для внешних переменных в файле src/config/.env.dev.
  • Репо определило и использует (через AuthController и UsersController) следующие конечные точки.
  • auth/signup
    auth/signin
    users/
    users?
    users/:id/

Он также использует глобальный префикс /tickets, поэтому для доступа к пользователям вы должны использовать: http://localhost:3000/tickets/users.

Получить репо на вашем компьютере

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

git clone https://github.com/zzpzaf/nest-external-config-env.git

Это создает новую папку с именем nest-external-config-env со всеми папками и файлами репо. Перейдите в него и получите все пакеты узлов, необходимые для проекта:

cd nest-external-config-env
npm i

Теперь вы готовы взять редактор кода/IDE. (использую VS Code) и открываем папку nest-external-config-env. При желании вы можете запустить приложение в окне терминала:

npm run start:dev

Более того, вы можете воспользоваться инструментом Почтальон и проверить некоторые конечные точки. например: http://localhost:3000/tickets/users

и т.д. Вот так! Теперь мы готовы начать. Итак, начнем!

Пакет @nestjs/jwt

@nestjs/jwt — модуль утилит JWT для NestJS (основан на пакете jsonwebtoken). Это позволяет нам использовать JSON Web Token — JWT для аутентификации пользователя.

Монтаж

npm i @nestjs/jwt

Выполнение

Пакет фактически предлагает нам модуль JwtModule, который, в свою очередь, предоставляет услугу JwtService.

JwtModule

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

Кроме того, мы также должны настроить его, используя метод register(). Базовая конфигурация может включать только симметричный секрет (код) и время истечения срока действия сгенерированного токена, например, в секундах. Например, мы можем использовать его так:

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

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

Также обратите внимание, что вы никогда не должны публично раскрывать свои пароли или секретные коды. Более того, вы должны включить в свой .gitignore любой файл с такой конфиденциальной информацией. Однако в данном случае это не так, для демонстрационных целей этого поста.

Итак, мы можем зарегистрировать JwtModule, используя внешние параметры, через ConfigService, вот так:

Стиль кодирования зависит от вас. [Вы всегда можете прочитать официальную документацию, чтобы узнать больше о динамических модулях и useFactory()]

JwtService

Затем JwtService, который выставляется из JwtModule, должен быть внедрен в AuthService (через конструктор AuthService), например:

Что мы на самом деле хотим сделать здесь, так это принять учетные данные пользователя (имя пользователя и текстовый пароль), предоставленные в теле запроса процесса «Войти», а затем проверить их только один раз. Только после успешной проверки мы можем попросить JwtService предоставить нам токен, который должен быть возвращен обратно пользователю в качестве ответа. В случае неудачной проверки (неправильное имя пользователя или пользователь не существует или неправильный пароль) мы можем сообщить пользователю об этом.

Полезная нагрузка JWT

Еще одна вещь, на которой мы должны немного остановиться, — это определение полезной нагрузки. Обычно это объект JSON, необходимый для создания JWT. [Подробнее на: https://jwt.io/introduction/]

Определение полезной нагрузки зависит от вашей собственной бизнес-логики и ваших конкретных потребностей. Но в любом случае избегайте слишком простой полезной нагрузки. Пример: состоит только из имени пользователя.

Здесь мы собираемся использовать объект JSON с двумя свойствами в качестве примера полезной нагрузки. Он должен состоять из имени пользователя и typeid. Таким образом, помимо действительности имени пользователя и пароля, пользователь должен иметь соответствующий typeid, чтобы иметь возможность доступа к конечным точкам API.

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

Обратите внимание, что в нашем клонированном репозитории мы используем отдельную подпапку (папка datObjects), которая содержит все объекты типов данных нашего проекта. Итак, создайте новый файл TypeScript, назовите его user-jwt-payload.interface.ts и поместите в него следующий пример кода:

Создать и вернуть JWT

Теперь мы готовы проверить (или нет) пользователя, и если проверка прошла успешно, мы можем вернуть ему/ей токен. Мы можем сделать это довольно легко, используя JwtService в методе signin() в AuthService, например:

Как видите, мы использовали только что использованную встроенную функцию JwtService sign(), передав ей объект полезной нагрузки. Функция возвращает нам токен, который затем отправляется обратно пользователю в качестве объекта ответа. Это так просто! Не так ли? Вы можете использовать Postman для проверки. Например:

Кроме того, вы также можете проверить сгенерированный токен онлайн на https://jwt.io/. Пример:

Вы также можете проверить недействительные учетные данные:

Примечание. Чтобы было ясно, что возвращаемый объект является токеном (JWT), а не просто сообщением (строкой), переименуйте accessMessage в accessToken как в AutService, так и в AuthController.

Проверка через JWT

Теперь каждый раз, когда мы хотим проверить запрос от пользователя в конечной точке, мы будем проверять действительность только предоставленного токена (JWT, предоставленный как Bearer Token в заголовке авторизации запроса). Больше никаких логинов и паролей!

JwtService с помощью встроенной функции verifyAsync() выполнит эту работу! Давайте посмотрим, как это можно сделать для конкретной конечной точки, например: для пользователей/конечная точка. Однако сначала мы должны извлечь токен Bearer, указанный в заголовке авторизации (ну, если он есть).

Мы можем сделать это, например, для метода-обработчика @Get() getUsers() в файле UsersController. Мы будем использовать декоратор @Headers для получения всех переданных заголовков, а затем извлечем токен Bearer из свойства заголовка authorization. Мы можем сделать что-то вроде этого:

Затем мы можем определить и вызвать закрытый метод для проверки достоверности полученного токена носителя, используя verifyAsync(), упомянутый выше.

Поскольку нам понадобятся как JwtService, так и ConfigService (для получения секретного ключа, который также должен быть предоставлен в функции verifyAsync()), нам нужно выполнить 2–3 корректировки кода, прежде чем мы продолжим.

В клонированном репозитории UsersModule вы можете видеть, что мы уже позаботились об импорте AuthModule, однако, чтобы иметь возможность использовать/внедрить JwtService, мы должны экспортировать JwtModule из AuthModule. Итак, добавьте в него массив свойств exports и добавьте файл JwtModule. AuthModule должен выглядеть так:

Теперь мы готовы внедрить JwtService в наш UsersController.

Как мы уже говорили, нам также нужно будет ввести ConfigService. Но так как он включен глобально в AppModule (isGlobal: true), то больше делать нечего. Мы также готовы внедрить его в UsersController.

Итак, давайте введем их обоих. Конструктор UsersController будет похож на:

Наконец, мы можем создать закрытый метод для проверки достоверности полученного носителя. Мы можем назвать его isTokenValid:

Как легко понять, мы передаем в функцию jwtService.verifyAsync() 2 параметра: токен носителя и секрет JWT.

Если токен-носитель недействителен, метод выдает HttpException, в противном случае он возвращает объект с полной расшифрованной полезной нагрузкой. Объект полезной нагрузки состоит из: имени пользователя, typeid (которые определяются нами при создании токена) и свойств iat и exp, которые автоматически добавляются файлом JwtService. iat представляет собой создание, а exp — время истечения срока действия токена (разница, как и ожидалось, составляет 3600 секунд).

Что нам действительно нужно от возвращаемого объекта полезной нагрузки, так это только username и typeid.

username необходимо проверить, существует ли еще пользователь с username. Потому что всегда есть шанс, что пользователь будет удален после того, как токен был сгенерирован. Итак, мы должны проверить существование пользователя, а затем решить, действителен ли токен. Таким образом, мы используем «имя пользователя», чтобы проверить, существует ли пользователь в нашем DbRepo.

После этого проверяем файл typeid. В нашем случае токен считается действительным, только для пользователей с typeid 1 и 2. Если это правда, то IsTokenValid() возвращает true.

Обратите внимание, что мы остаемся stuck со значением typeid, полученным из полезной нагрузки (а не из свойства user.typeid от найденного пользователя), поэтому любые изменения значения typeid игнорируются.

Итак, давайте вызовем его из обработчика getUsers().

Обработчик становится похожим на:

Вот и все. Теперь мы можем использовать Postman и немного протестировать его.
Это ответ с действительным токеном носителя:

И это недопустимый пример токена (время истекло):

Вроде нормально. Итак, что мы можем сделать дальше? Мы можем повторить вызов isTokenValid() для других конечных точек и операций CRUD. Но, как вы понимаете, это не соответствует принципу «DRY». Он требует большого количества шаблонного кода, и поэтому его нелегко поддерживать. Можете себе представить, что это значит, если позже мы решим добавить больше конечных точек и использовать много других модулей?

Нет, это не то, чем мы будем заниматься. Вместо этого мы будем использовать пакет Passport.

Прежде чем продолжить, и, если хотите, сделайте коммит. В качестве альтернативы вы можете загрузить или клонировать чистый репозиторий с тем, что мы описали до сих пор о реализации @nestjs/jwt, для справки. Вот это https://github.com/zzpzaf/nest-external-config-env-jwt. Обратите внимание, что мы собираемся удалить части кода, использованного выше.

Обратите внимание, что мы собираемся удалить части кода, использованного выше.

Пакет(ы) Паспорт

Монтаж

npm i passport @nestjs/passport passport-jwt
npm i --save-dev @types/passport-jwt

Выполнение

Импортируйте его в AuthModule . Начнем с импорта PassportModule в секцию массива импорта в наш AuthModule, аналогично тому, как мы делали это раньше для JwtModule. Здесь снова мы должны использовать встроенный метод register(), предоставленный Passport, для регистрации стратегии в качестве стратегии по умолчанию для нашего проекта. Как вы, наверное, догадались, стратегия по умолчанию — Стратегия JWT. Итак, наш AuthModule становится:

Как вы можете, мы добавили всего одну строку кода:

PassportModule.register({ defaultStrategy: 'jwt' }),

Следующим шагом является реализация такой Стратегии JWT по-своему. Собственно, наша собственная стратегия должна быть реализована через оформленный класс @injectable, который, в свою очередь, должен расширять класс PassportStrategy(Strategy) пакета Passport.

Мы будем использовать отдельный файл для этого класса. Мы назовем файл jwt.strategy.ts и класс JwtStrategy. Мы можем поместить этот файл в папку src/auth нашего проекта:

Конструктор — вызов родителя: super()

Чтобы использовать его, мы должны передать объект как минимум с двумя параметрами в его конструкторе. А точнее, поскольку это производный класс (от класса PassportStrategy(Strategy)), мы также должны вызывать класс super (parent) из класса Constructor в конструктор родительского класса — поэтому мы должны сделать это в super({}) этого класса. Метод super() требует 2 параметра:

constructor(){super({param1: value1, param2: value2})}

1-й параметр — jwtFromRequest (обязательный) — этот параметр необходим, потому что мы должны извлечь токен JWT Bearer (полезную нагрузку) из заголовка запроса, аналогично тому, что мы делали раньше вручную. На самом деле это функция, которая принимает запрос в качестве единственного параметра и возвращает либо JWT в виде строки, либо null. Дополнительные сведения см. в разделе Извлечение JWT из запроса. Мы можем использовать параметры, предоставляемые через перечисления ExtractJwt. В нашем случае мы должны выбрать fromAuthHeaderAsBearerToken():

2-й параметр (Необходим для 1-го). — secretOrKey — Это строка или буфер, содержащий секретный (симметричный) или закодированный в PEM открытый ключ (асимметричный) для проверки подписи токена. Здесь мы будем использовать наш секрет, который мы также использовали ранее. Конечно, в нашем случае мы должны получить его из ConfigService., поэтому мы также должны внедрить ConfigService в конструктор класса. Итак, вызов суперконструктора должен выглядеть так:

Метод validate() должен быть частью класса реализации PassportStrategy. Для каждой стратегии Passport по умолчанию вызывает функцию проверки jwt. В NestJS эта функция реализована с помощью метода validate().

Если этот метод опущен, мы получим ошибку исключения:
ERROR [ExceptionsHandler] this.validate is not a function

Мы используем функцию validate(), которая должна быть промисом, просто для того, чтобы вернуть что-то после того, как мы внедрили нашу собственную логику проверки. Например, мы можем всегда возвращать true (для проверки всего):

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

В нашем случае мы используем функцию validate() для возврата пользовательского объекта. Чтобы вернуть пользовательский объект, у нас должен быть доступ к DbRepo и его методам. Итак, мы должны внедрить DbRepo в конструктор JwTStrategy. Наш JwtStrategy должен быть похож на:

После того, как мы определили наш JwtStrategy, следующим шагом будет добавление этой стратегии в массив свойств providers в файле AuthModule. Затем мы также добавляем его в массив свойств экспорта вместе с модулем паспорта. Таким образом, мы делаем их доступными в другом месте и для других модулей:

Вот и все! Теперь мы готовы к последнему шагу и используем Passport для проверки токенов JWT, не делая это «вручную», как мы это сделали (в UsersController).

Следующим шагом будет защита наших маршрутов — конечных точек с помощью встроенного метода Passport: AuthGuard().

Краткое введение в механизм Guards в Nest.js

Прежде чем продолжить, мы должны упомянуть, что фреймворк NestJS предоставляет родную функциональность Guards. Как правило, мы можем определить/реализовать наш собственный класс AuthGuard (который реализует интерфейс NestJS CanActivate) и настроить для него функцию canActivate(). Затем мы можем применить (связать) его с помощью декоратора @UseGuards. Guard может быть привязан двумя способами в NestJS:

  1. Защитите весь маршрут контроллера.
  2. Защитить конкретный обработчик (в контроллере)

Например, мы можем создать и защитить определенный обработчик @Post. Например, в наш auth.controller.ts мы можем добавить это:

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

Выше мы использовали декоратор NestJS @UseGuards и только что передали ему пользовательский класс AuthGuard, чтобы защитить только обработчик POST конечной точки /test.

Но мы также можем защитить весь маршрут на уровне контроллера. И это как кусок пирога с использованием паспорта Authguard(). Вот это наш случай. Мы можем защитить весь маршрут UsersController, просто добавив:

@UseGuards(AuthGuard())

Пример:

Теперь запрос на маршрут /users не может быть обработан без аутентификации, то есть без действительного токена JWT. AuthGuard() автоматически проверяет действительность токена JWT с помощью нашего определения JwtStrategy и принимает запрос к любому из UsersController обработчиков методов HTTP или отклоняет его.

Последний шаг — очистить обработчик метода UsersController getUsers() от кода, который мы ранее создали для проверки токена manual JWT. Ниже мы только что порекомендовали его, поэтому позже вы можете полностью удалить его:

И это результат теста через Postman:

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

Вы можете получить доступ к готовому чистому репо по адресу: https://github.com/zzpzaf/nest-external-config-env-jwt-passport.git.

Вот и все! Надеюсь, вам понравилось, несмотря на то, что это был немного длинный пост! Удачного кодирования! … и следите за обновлениями!