Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Модуль 8 «Ограничение доступа» #8

Merged
merged 18 commits into from
Feb 27, 2024
Merged

Conversation

AntonovIgor
Copy link
Contributor

No description provided.

Следующим шагом добавим в приложение поддержку авторизации. Есть разные способы решения этой задачи. Мы воспользуемся JWT (JSON Web Token).

Для работы с JWT есть несколько готовых пакетов. Наиболее популярный — `jsonwebtoken`. Он поддерживает большое количество алгоритмов шифрования и можно сказать проверен временем.

Однако, у него не очень удобный интерфейс. Он построен на функциях обратного вызова,
колбеках. Можно самостоятельно сделать все необходимые обёртки, но мы пойдём немного другим путём. Воспользуемся более современным вариантом — `jose`.

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

Установим пакет `jose` в качестве основной зависимости. Типы поставляются из коробки, поэтому дополнительно их указывать не нужно.
При подготовке токена нам потребуется подготовить секрет — строка, которая будет использоваться в процессе шифрования. За время работы приложения она может меняться. Будем передавать её в переменной
окружения. Заведём в конфигурации отдельную переменную окружения `JWT_SECRET`. Значения по умолчанию не будет (`null`). Затем добавим её в `.env`.
В классе `UserEntity` мы реализовали метод `setPassword` для формирования хеша на основе пароля. Пришло время добавить
дополнительный метод, который сможет сравнить пароли. Идея в следующем.

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

Полученный пароль хешируем с помощью функции `createSHA256`, а затем сравним его с хешем, который сохранён в базе.
Если хэши паролей совпадают, значит пользователь ввёл корректный пароль и его можно пропускать дальше.
Для реализации проверки логина и пароля, а также формирования токена подготовим отдельный модуль `Auth`. В модуле предусмотрим методы

* `authenticate`. Аргументом принимает `UserEntity`. Результатом становится токен.
* `verify`.  Выполняет аутентификацию. На вход принимает `LoginUserDto`. Результатом станет `UserEntity` или ошибка.

В этом же коммите добавим константы `JWT_ALGORITHM` и `JWT_EXPIRED`. В первой зафиксируем алгоритм для формирования токена, а во второй время, в течение которого токен считается валидным.

WIP: Задача в процесс решения.
…eption`. WIP

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

Есть разные способы обработки подобных ситуаций. Методы сервиса `Auth` могут возвращать, например, значение `null`, если пользователь не найден. Можно обработать это значение где-то в месте обращения к серверу и подготовить соответствующий ответ клиенту.

Можно пойти другим путём и описать отдельные виды ошибок. Определяем для каждой ситуации ошибки, бросаем их при наступлении определённого события и дальше где-нибудь их обрабатываем.
А как обработать отдельные ошибки? Те, что были созданы в предыдущем коммите. Можно расширить ранее созданный `AppExceptionFilter`. Мы можем добавить отдельные методы и обрабатывать по-своему новые виды ошибок.

Однако, этот подход отразится на поддержке `AppExceptionFilter`. Он станет слишком большим. Выход из этой ситуации: создание отдельного `AuthExceptionFilter`. Разместим его в директории `Auth`.
Теперь подготовим инфраструктуру для нового модуля `Auth`. Соберём все необходимые зависимости в контейнере, внедрим нужные компоненты в `RestApplication`, зарегистрируем новый фильтр исключений, добавим компоненты в `Component` и так далее.
Теперь внедрим в качестве зависимости новый сервис в `UserController` и имплементируем обработчик `login`. Если всё прошло хорошо, то он вернёт новый токен. Проверить токен на корректность и посмотреть его содержимое вы можете на сайте https://jwt.io/.

При тестировании попробуйте воспроизвести негативные сценарии: отправьте некорректные логин или пароль. `AuthExceptionFilter` должен перехватить ошибки.
Токены формируются. Теперь надо как-то извлечь информацию из `payload` токена. Это можно делать на уровне обработчика маршрута, но в этом случае, один и тот же код придётся дублировать в разных местах
приложения. Чтобы этого избежать, создадим `ParseTokenMiddleware`,
которую зарегистрируем глобально. Если к запросу будет приложен токен, то middleware сможет извлечь из него информацию и добавить к запросу.

Начнём с извлечения информации. Сначала проверим наличие заголовка `authorization`. Если он есть, значит продолжаем работать. Затем
извлечём сам токен. После этого проверим с помощью `jwtVerify`, Если всё ок, то сохраним в свойство запроса `user`. Таким образом, когда
запрос долетит до обработчика маршрута, у нас будет вся необходимая информация.
Создадим файл `custom.d.ts` и расширим информацию о типе `Request`. Обновим конфигурационный файл `tsconfig.json`.
Теперь воспользуемся middleware и будем получать ID пользователя автоматически.
Токены работают, но по-прежнему есть проблема: клиент может
обращаться ко всем маршрутам. Такого быть не должно. Некоторые маршруты априори приватны. Реализуем дополнительную проверку. Для этого создадим новый middleware — `PrivateRouteMiddleware` и в нём выполним проверку. Если в объекте запроса нет объекта `tokenPayload`, значит не авторизованы.
Последним шагом добавим `PrivateRouteMiddleware` в обработчике `create`.
Теперь, маршрут для создания комментария стал приватным. К нему нельзя выполнить запрос без токена.
К обработчикам маршрутов, которые отвечают за создание данных подключим
middleware `PrivateRouteMiddleware`. К этим ресурсам могут обращаться
только авторизованные клиенты.
После добавления ограничений для маршрутов внесём исправления в обработчик создания нового объявления. Вместо ожидания идентификатора автора объявления от клиента, подставим его самостоятельно. Затем откроем `CreateOfferDto` и уберём валидатор с `userId`. Больше он не потребуется.
Фронтенду необходима возможность проверять актуальность JWT-токена и
если токен актуален, возвращать информацию о пользователе. Сделать такой ресурс несложно. Добавим обработчик для маршрута `GET /login` в
`UserController`.
Добавим запрос для ресурса `GET /login`, чтобы клиент мог проверить токен на актуальность.
@AntonovIgor AntonovIgor self-assigned this Feb 20, 2024
@AntonovIgor AntonovIgor merged commit fe03ced into main Feb 27, 2024
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant