# JWT-авторизация Продолжаем разработку АПИ для проекта "ресторан" используя **express.js**. **Содержание:** * [Создание проекта](./express01.md#создание-проекта) * [Подключение к БД](./express02.md#подключение-к-бд) * [Использование sequelize для получения данных из БД](./express03.md#использование-sequelize-для-получения-данных-из-бд) * [Создание скрипта для наполнения БД начальными данными](./express03.md#создание-скрипта-для-наполнения-бд-начальными-данными) * [Добавление сущностей БД, реализация REST.](./express03.md#добавление-сущностей-бд-реализация-rest) * [JWT-авторизация](#jwt-авторизация) >JSON Web Token (JWT) — это JSON объект, который определен в открытом стандарте RFC 7519. Он считается одним из безопасных способов передачи информации между двумя участниками. Для его создания необходимо определить заголовок (header) с общей информацией по токену, полезные данные (payload), такие как `id` пользователя, его `роль` и т.д. и подписи (signature). ## Структура JWT __JWT__ состоит из трех частей: заголовок `header`, полезные данные `payload` и подпись `signature`. Давайте пройдемся по каждой из них. ### Шаг 1. Создаем HEADER Хеадер __JWT__ содержит информацию о том, как должна вычисляться __JWT__ подпись. Хеадер — это тоже __JSON__ объект, который выглядит следующим образом: ```json { "alg": "HS256", "typ": "JWT"} ``` Поле `typ` не говорит нам ничего нового, только то, что это _JSON Web Token_. Интереснее здесь будет поле `alg`, которое определяет алгоритм хеширования. Он будет использоваться при создании подписи. HS256 — не что иное, как HMAC-SHA256, для его вычисления нужен лишь один секретный ключ (более подробно об этом в шаге 3). Еще может использоваться другой алгоритм RS256 — в отличие от предыдущего, он является ассиметричным и создает два ключа: публичный и приватный. С помощью приватного ключа создается подпись, а с помощью публичного только лишь проверяется подлинность подписи, поэтому нам не нужно беспокоиться о его безопасности. ### Шаг 2. Создаем PAYLOAD `Payload` — это полезные данные, которые хранятся внутри **JWT**. Эти данные также называют JWT-claims (заявки). В примере, который рассматриваем мы, сервер аутентификации создает **JWT** с информацией об `id` пользователя — `userId`. ```json { "userId": "b08f86af-35da-48f2-8fab-cef3904660bd" } ``` Мы положили только одну заявку (claim) в `payload`. Вы можете положить столько заявок, сколько захотите. Существует список стандартных заявок для JWT payload — вот некоторые из них: * iss (issuer) — определяет приложение, из которого отправляется токен. * sub (subject) — определяет тему токена. * exp (expiration time) — время жизни токена. Эти поля могут быть полезными при создании **JWT**, но они не являются обязательными. Если хотите знать весь список доступных полей для **JWT**, можете заглянуть в **Wiki**. Но стоит помнить, что чем больше передается информации, тем больший получится в итоге сам **JWT**. Обычно с этим не бывает проблем, но все-таки это может негативно сказаться на производительности и вызвать задержки во взаимодействии с сервером. ### Шаг 3. Создаем SIGNATURE Подпись вычисляется с использованием следующего псевдо-кода: ``` const SECRET_KEY = 'cAtwa1kkEy' const unsignedToken = base64urlEncode(header) + '.' + base64urlEncode(payload) const signature = HMAC-SHA256(unsignedToken, SECRET_KEY) ``` Алгоритм _base64url_ кодирует `хедер` и `payload`, созданные на 1 и 2 шаге. Алгоритм соединяет закодированные строки через точку. Затем полученная строка хешируется алгоритмом, заданным в хедере на основе нашего секретного ключа. ``` // header eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 // payload eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ // signature -xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM ``` ### Шаг 4. Теперь объединим все три JWT компонента вместе Теперь, когда у нас есть все три составляющих, мы можем создать наш **JWT**. Это довольно просто, мы соединяем все полученные элементы в строку через точку. ```js const token = encodeBase64Url(header) + '.' + encodeBase64Url(payload) + '.' + encodeBase64Url(signature) // JWT Token // eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM ``` Вы можете попробовать создать свой собственный JWT на сайте [jwt.io](jwt.io). Вернемся к нашему примеру. Теперь сервер аутентификации может слать пользователю __JWT__. ## Как JWT защищает наши данные? Очень важно понимать, что использование **JWT** НЕ скрывает и не маскирует данные. Причина, почему **JWT** используются — это проверка, что отправленные данные были действительно отправлены авторизованным источником. Как было продемонстрировано выше, данные внутри **JWT** закодированы и подписаны, обратите внимание, это не одно и тоже, что зашифрованы. Цель кодирования данных — преобразование структуры. Подписанные данные позволяют получателю данных проверить аутентификацию источника данных. Таким образом закодирование и подпись данных не защищает их. С другой стороны, главная цель шифрования — это защита данных от неавторизованного доступа. ## Простыми словами 1. При авторизации по логину/паролю сервер убеждается, что такой пользователь есть, формирует **JWT**-токен, содержащий полезную информацию и подпись, и возвращает этот токен клиенту (сайт, приложение) ```mermaid sequenceDiagram actor Клиент Клиент->>АПИ: Запрос авторизации по логину/паролю activate АПИ %%box purple Запрос к базе данных %%participant DB@{ "type" : "database" } %%end АПИ->>DB: Поиск пользователя по логину activate DB alt Пользователь существует DB->>АПИ: Информация о пользователе АПИ->>АПИ: Формирование токена АПИ->>Клиент: JWT-токен else Пользователя нет в базе DB->>АПИ: NULL АПИ->>Клиент: Ошибка 404 (пользователь не найден) end deactivate DB deactivate АПИ ``` 1. Клиент при последующих запрсах добавляет **JWT**-токен в заголовок запроса: ``` Authorization: Bearer ``` 1. Сервер, получая **JWT**-токен уже не лезет в таблицу пользователей, а просто проверяет подпись токена, убеждаясь что она не подделана (ключ подписи есть только у севрера). А затем может использовать полезную информацию из токена в запросах к БД. Таким образом мы экономим на каждом входящем запросе одно обращение к БД (а работа с БД самое медленное действие, вычисление подписи намного дешевле) ```mermaid sequenceDiagram actor Клиент Клиент->>АПИ: Запрос с токеном activate АПИ АПИ->>АПИ: Проверка токена alt Подпись валидная АПИ->>АПИ: Обработка запроса АПИ->>Клиент: Ответ на запрос else Подпись не валидная АПИ->>Клиент: Ошибка 403 (доступ запрещен) end deactivate АПИ ``` ## Подключаем JWT-авторизацию к нашему проекту ### Устанавливаем необходимые пакеты: ``` npm i jsonwebtoken md5 ``` * **jsonwebtoken** - нужен для формирования и проверки токена * **md5** - для хеширования пароля ### Подключаем зависимости ```js const md5 = require('md5') const jwt = require('jsonwebtoken') ``` ### Задаём подпись Этот параметр тоже нельзя "светить", поэтому считываем из переменных окружения (добавьте в `jaunch.json`) ```js const JWT_SECRET = process.env.JWT_SECRET ``` ### Реализуем метод авторизации ```js /** * POST /api/user/login * В теле запроса должен быть объект с логином и паролем: * { * "login": "ваш логин", * "password": "пароль" * } */ router.post('/login', async (req, res) => { try { /* const user = await sequelize.query(` SELECT * FROM User WHERE login=:login `, { // параметр plain нужен, чтобы запрос вернул не массив записей, а конкретную запись // если записи с таким логином нет, то вернет null plain: true, logging: false, type: QueryTypes.SELECT, replacements: { login: req.body.login } }) */ // у меня нет таблицы User, поэтому я сделал заглушку // вам нужно брать данные для авторизации из БД const user = { password: md5('123456'), id: 1, roleId: 1 } if (user) { // хешируем пароль const passwordMD5 = md5(req.body.password) if (user.password == passwordMD5) { // формируем токен const jwtToken = jwt.sign({ id: user.id, firstName: user.firstName, roleId: user.roleId }, JWT_SECRET ) res.json(jwtToken) } else { res.status(401).send('не верный пароль') } } else { res.status(404).send('пользователь не найден') } } catch (error) { console.warn('ошибка при авторизации:', error.message) res.status(500).send(error.message) } finally { res.end() } }) ``` ### Реализуем аутентификацию пользователя в запросе корзины, используя middleware **Middleware** функция принимает на входе параметры `req`, `res`, `next`, где `req`, `res` соответственно запрос и ответ, а `next` нужно вызвать, если всё нормально и можно вызывать следующую middleware функцию (их может быть несколько) ```js /** * Middleware авторизации */ const authenticateJWT = (req, res, next) => { const authHeader = req.headers.authorization if (authHeader) { const token = authHeader.split(' ') if (token[0].toLowerCase() != 'bearer') return res.status(400).send('не поддерживаемый тип авторизации') jwt.verify(token[1], JWT_SECRET, (err, data) => { // если в результате есть ошибка (err) // то возвращаем статус forbidden if (err) return res.status(403).send(err) // иначе сохраняем полезные данные в объект user запроса req.user = data next() }) } else { res.status(401).send('нет заголовка авторизации') } } ``` Теперь добавляем эту функцию в запрос получения корзины (обратите внимание, у меня в бд нет пользователей, поэтому я закомментировал часть кода, в вашем проекте нужно раскомментировать): ```js /** * GET /api/cart * Вторым параметром метода добавлен массив middleware */ router.get('/', [authenticateJWT], async (req, res) => { try { res.json(await sequelize.query(` SELECT * FROM Cart WHERE userId=:userId `, { logging: false, type: QueryTypes.SELECT replacements: { userId: req.user.id // ^^^^ обратите внимание, // тут используется объект user, // заполненный в middleware аутентификации } })) } catch (error) { console.error(error) } finally { res.end() } }) ``` ## Задание Реализовать авторизацию по JWT-токену в своем АПИ