|
|
@@ -11,12 +11,11 @@
|
|
|
* [Добавление сущностей БД, реализация REST.](#добавление-сущностей-бд-реализация-rest)
|
|
|
* [JWT-авторизация](#jwt-авторизация)
|
|
|
|
|
|
-
|
|
|
Для подключения к БД можно использовать пакет `mysql`, но для более-менее сложных проектов, подразумевающих дальнейшее развитие, лучше использовать библиотеки, поддерживающие миграции (инициализация и изменение структуры базы данных). Для **JavaScript** наиболее популярна __ORM__ библиотека [sequelize](https://sequelize.org/). На [хабре](https://habr.com/ru/articles/565062/) есть цикл статей, посвящённый этой библиотеке.
|
|
|
|
|
|
Всеми возможностями мы пользоваться не будем, нам достаточно миграций.
|
|
|
|
|
|
-### Установим зависимости
|
|
|
+## Установим зависимости
|
|
|
|
|
|
>Выполняем в каталоге с проектом
|
|
|
|
|
|
@@ -26,11 +25,11 @@ npm i mysql2 sequelize sequelize-cli
|
|
|
|
|
|
* Пакет `mysql2` нужен для работы с БД MySQL (явно мы его не используем, но он нужен для **sequelize**)
|
|
|
* Пакет `sequelize` нужен для использования в нашем коде
|
|
|
-* Консольная утилита `sequelize-cli` нужно для создания и управление миграциями
|
|
|
+* Консольная утилита `sequelize-cli` нужна для создания и управления миграциями
|
|
|
|
|
|
-### Инициализация и настройка sequelize (только перед первым запуском)
|
|
|
+## Инициализация и настройка sequelize (только перед первым запуском)
|
|
|
|
|
|
-#### Инициализация:
|
|
|
+### Инициализация:
|
|
|
|
|
|
```
|
|
|
npx sequelize-cli init
|
|
|
@@ -43,9 +42,9 @@ npx sequelize-cli init
|
|
|
* `migrations` — файлы с миграциями
|
|
|
* `seeders` — файлы для заполнения БД начальными данными
|
|
|
|
|
|
-#### Настройка
|
|
|
+### Настройка
|
|
|
|
|
|
-Далее нам нужно сообщить консольной утилите (`sequelize-cli`), как подключиться к БД. Для этого откроем файл `config/config.json`. Он выглядит примерно так:
|
|
|
+Далее нам нужно сообщить консольной утилите (`sequelize-cli`), как подключаться к БД. Для этого откроем файл `config/config.json`. Он выглядит примерно так:
|
|
|
|
|
|
```json
|
|
|
{
|
|
|
@@ -98,10 +97,22 @@ npx sequelize-cli init
|
|
|
}
|
|
|
```
|
|
|
|
|
|
+>В каталоге `models` есть файл `index.js` в котором происходит инициализация подключения, нам в нём ничего менять не нужно, но я приведу кусок кода из него, чтобы было понятно где используется параметр `use_env_variable`
|
|
|
+>
|
|
|
+>```js
|
|
|
+>if (config.use_env_variable) {
|
|
|
+> sequelize = new Sequelize(process.env[config.use_env_variable], config);
|
|
|
+>} else {
|
|
|
+> sequelize = new Sequelize(config.database, config.username, config.password, config);
|
|
|
+>}
|
|
|
+>```
|
|
|
+>
|
|
|
+>То есть, если в конфиге есть параметр `use_env_variable`, то в конструктор передается строка подключения из переменной окружения, которая задана в этом параметре (в нашем случае `DATABASE_URL`). Иначе используется конструктор с параметрами `database`, `username` и `password`.
|
|
|
+
|
|
|
**Sequelize** поддерживает загрузку строки подключения из переменных окружения. Нам нужно создать переменную окружения в таком формате:
|
|
|
|
|
|
```
|
|
|
-DATABASE_URL=mysql://[user]:[pass]@[sqldomain]/[db name]
|
|
|
+DATABASE_URL=mysql://user:password@sqldomain/db_name
|
|
|
```
|
|
|
|
|
|
В **VSCode** можно задать переменные в настройках:
|
|
|
@@ -114,15 +125,20 @@ DATABASE_URL=mysql://[user]:[pass]@[sqldomain]/[db name]
|
|
|
|
|
|

|
|
|
|
|
|
-### Создание базы данных
|
|
|
+## Создание базы данных
|
|
|
+
|
|
|
+>Базу можно создать и в менеджере баз данных, но сделаем в консоли, чтобы проверить все настройки
|
|
|
+
|
|
|
+Для создания БД используется команда:
|
|
|
|
|
|
```
|
|
|
npx sequelize-cli db:create [--url=<строка подключения>]
|
|
|
```
|
|
|
|
|
|
-`--url <строка подключения к БД>` можно не указывать, если:
|
|
|
+Параметр `--url <строка подключения к БД>` можно не указывать, если:
|
|
|
+
|
|
|
+* оставили стандартный вариант инициализации БД или в переменных окружения есть `DATABASE_URL` (учитывайте, что на командную строку не распространяются настройки VSCode)
|
|
|
|
|
|
-* оставили стандартный вариант инициализации БД или в переменных окружения есть `DATABASE_URL` (учитывайте, что на командную строку не распространяются настойки VSCode)
|
|
|
* в корне проекта создан файл `.sequelizerc`, в котором задана переменная `url`:
|
|
|
|
|
|
```js
|
|
|
@@ -131,25 +147,27 @@ npx sequelize-cli db:create [--url=<строка подключения>]
|
|
|
}
|
|
|
```
|
|
|
|
|
|
- Такой вариант предпочтительнее, чтобы не писать `url` при каждой миграции. Только не забудьте и этот файл прописать в `.gitignore`
|
|
|
+ Такой вариант предпочтительнее, чтобы не писать `url` при каждой миграции (мы рассматриваем вариант, когда в конфиге не хранятся логин и пароли, а используется переменная окружения). Только не забудьте и этот файл прописать в `.gitignore`
|
|
|
|
|
|
-Если всё настроили правильно, то при запуске выдаст примерно такое:
|
|
|
+Если всё настроили правильно, то при запуске команды `npx sequelize-cli db:create` выдаст примерно такое (и на сервере MySQL будет создана БД, которая указана в строке подключения):
|
|
|
|
|
|
```
|
|
|
-> npx sequelize-cli db:create
|
|
|
-
|
|
|
Sequelize CLI [Node: 23.9.0, CLI: 6.6.3, ORM: 6.37.7]
|
|
|
|
|
|
Parsed url mysql://ekolesnikov:*****@kolei.ru/restaurant
|
|
|
Database restaurant created.
|
|
|
```
|
|
|
|
|
|
-### Создадим миграцию для начальной инициализации базы данных
|
|
|
+## Создадим миграцию для начальной инициализации базы данных
|
|
|
+
|
|
|
+Миграции создаются командой:
|
|
|
|
|
|
```
|
|
|
npx sequelize-cli migration:generate --name first
|
|
|
```
|
|
|
|
|
|
+>Вместо `first` лучше писать что-то человеко-понятное, например `create_MenuItem`
|
|
|
+
|
|
|
В каталоге `migrations` будет создан файл `YYYYMMDDhhmmss-first.js`:
|
|
|
|
|
|
```js
|
|
|
@@ -177,7 +195,7 @@ module.exports = {
|
|
|
};
|
|
|
```
|
|
|
|
|
|
-В методе `up` мы должны прописать команды для создания таблиц и связей, а в методе `down` для удаления. В первой минграции мы создадим таблицу `MenuItem` для хранения блюд (тут вроде все более менее понятно, подробно расписывать не буду - если что-то не понятно, то можно загуглить):
|
|
|
+В методе `up` мы должны прописать команды для создания таблиц и связей, а в методе `down` для удаления. В первой миграции мы создадим таблицу `MenuItem` для хранения блюд (тут вроде всё более менее понятно, подробно расписывать не буду - если что-то не понятно, то можно загуглить):
|
|
|
|
|
|
```js
|
|
|
'use strict';
|
|
|
@@ -244,10 +262,31 @@ Parsed url mysql://ekolesnikov:*****@kolei.ru/restaurant
|
|
|
npx sequelize-cli db:migrate:undo
|
|
|
```
|
|
|
|
|
|
-Приемры создания внешних ключей в миграции:
|
|
|
+Дополнительные примеры создания миграций:
|
|
|
|
|
|
```js
|
|
|
async up (queryInterface, Sequelize) {
|
|
|
+ // создаём словарь
|
|
|
+ await queryInterface.createTable('NewsType', {
|
|
|
+ id: {
|
|
|
+ allowNull: false,
|
|
|
+ autoIncrement: true,
|
|
|
+ primaryKey: true,
|
|
|
+ type: Sequelize.DataTypes.INTEGER
|
|
|
+ },
|
|
|
+ title: {
|
|
|
+ type: Sequelize.DataTypes.STRING(16),
|
|
|
+ allowNull: false
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ // заполняем словарь данными
|
|
|
+ await queryInterface.bulkInsert('NewsType',[
|
|
|
+ {id: 1, title: 'Срочное'},
|
|
|
+ {id: 2, title: 'Важное'}
|
|
|
+ ])
|
|
|
+
|
|
|
+ // создаём таблицу, в которой будет использоваться словарь
|
|
|
await queryInterface.createTable('News', {
|
|
|
id: {
|
|
|
allowNull: false,
|
|
|
@@ -255,513 +294,57 @@ async up (queryInterface, Sequelize) {
|
|
|
primaryKey: true,
|
|
|
type: Sequelize.DataTypes.INTEGER
|
|
|
},
|
|
|
+ // поле для внешнего ключа
|
|
|
newsTypeId: {
|
|
|
- type: Sequelize.DataTypes.INTEGER,
|
|
|
- allowNull: true
|
|
|
- },
|
|
|
- newsStatusId: {
|
|
|
type: Sequelize.DataTypes.INTEGER,
|
|
|
allowNull: false
|
|
|
}
|
|
|
+ // тут еще много всяких полей
|
|
|
})
|
|
|
|
|
|
+ // создаём внешний ключ
|
|
|
await queryInterface.addConstraint('News', {
|
|
|
fields: ['newsTypeId'],
|
|
|
type: 'foreign key',
|
|
|
+ // названия ключей должны быть уникальными
|
|
|
name: 'FK_News_NewsType',
|
|
|
references: {
|
|
|
table: 'NewsType',
|
|
|
field: 'id'
|
|
|
}
|
|
|
- })
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-## Использование sequelize для получения данных из БД
|
|
|
-
|
|
|
-Возвращаемся к нашему `app.js`
|
|
|
-
|
|
|
-Добавляем в начале файла импорт библиотеки
|
|
|
-
|
|
|
-```js
|
|
|
-const { sequelize } = require('./models')
|
|
|
-const { QueryTypes } = require('sequelize')
|
|
|
-```
|
|
|
-
|
|
|
-И поменяем _endpoint_ (я добавил префикс `/api`, он нам понадобится при настройке **nginx**, когда будем заворачивать апи в контейнер):
|
|
|
-
|
|
|
-```js
|
|
|
-app.get('/api/menu-item', async (req, res) => {
|
|
|
- try {
|
|
|
- res.json(await sequelize.query(`
|
|
|
- SELECT *
|
|
|
- FROM MenuItem
|
|
|
- `, {
|
|
|
- logging: false,
|
|
|
- type: QueryTypes.SELECT
|
|
|
- }))
|
|
|
- } catch (error) {
|
|
|
- console.error(error)
|
|
|
- } finally {
|
|
|
- res.end()
|
|
|
- }
|
|
|
-})
|
|
|
-```
|
|
|
-
|
|
|
-1. Запросы к БД асинхронные, поэтому заворачиваем код в async/await
|
|
|
-1. Список блюд, возвращаемый запросом `sequelize.query` заворачиваем в `res.json()`, это добавит в заголовок ответа `Content-Type: application/json`.
|
|
|
-
|
|
|
-Пока у нас таблица блюд пустая, поэтому в ответе тоже будет пустой массив
|
|
|
-
|
|
|
-## Создание скрипта для наполнения БД начальными данными
|
|
|
-
|
|
|
-**Sequelize** позволяет добавить записи в таблицы (можно использовать для первоначальной инициализации словарей или при тестировании)
|
|
|
-
|
|
|
-```
|
|
|
-npx sequelize-cli seed:generate --name menu-items
|
|
|
-```
|
|
|
-
|
|
|
-В каталоге `seeders` будет создан файл аналогичный файлам миграции (собственно и отличий между ними нет, просто логически выделены отдельно)
|
|
|
-
|
|
|
-С помощью метода `bulkInsert` формируем список блюд для добавления:
|
|
|
-
|
|
|
-```js
|
|
|
-'use strict';
|
|
|
-
|
|
|
-/** @type {import('sequelize-cli').Migration} */
|
|
|
-module.exports = {
|
|
|
- async up (queryInterface, Sequelize) {
|
|
|
- await queryInterface.bulkInsert('MenuItem', [
|
|
|
- {id: 1, title: 'Салат', image: 'Салат.jpg', price: 100},
|
|
|
- {id: 2, title: 'Суп', image: 'Суп.jpg', price: 100},
|
|
|
- {id: 3, title: 'Компот', image: 'Компот.jpg', price: 100}
|
|
|
- ])
|
|
|
- },
|
|
|
-
|
|
|
- async down (queryInterface, Sequelize) {
|
|
|
- await queryInterface.bulkDelete(
|
|
|
- 'MenuItem',
|
|
|
- null /* тут можно прописать условие WHERE */
|
|
|
- )
|
|
|
- }
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-И запускаем добавление данных командой
|
|
|
-
|
|
|
-```
|
|
|
-npx sequelize-cli db:seed:all [--url=...]
|
|
|
-```
|
|
|
-
|
|
|
-Можно откатить отдельный импорт, но накатить можно только всё разом. Поэтому для заполнения словарей лучше использовать _bulkInsert_ в обычной миграции, а seeders использовать для тестов.
|
|
|
-
|
|
|
-## Добавление сущностей БД, реализация REST.
|
|
|
-
|
|
|
-Добавим в БД таблицу _Cart_ (корзина) и связи между _Cart_ и _MenuItem_
|
|
|
-
|
|
|
-```
|
|
|
-npx sequelize-cli migration:generate --name cart
|
|
|
-```
|
|
|
-
|
|
|
-И пропишем в созданном файле команды для создания таблицы с внешним ключем и команды для удаления:
|
|
|
-
|
|
|
->В реальном проекте ещё нужно добавить таблицу пользователей и привязать корзину к пользователю
|
|
|
-
|
|
|
-```js
|
|
|
-'use strict';
|
|
|
-
|
|
|
-/** @type {import('sequelize-cli').Migration} */
|
|
|
-module.exports = {
|
|
|
- async up (queryInterface, Sequelize) {
|
|
|
- await queryInterface.createTable('Cart', {
|
|
|
- id: {
|
|
|
- allowNull: false,
|
|
|
- autoIncrement: true,
|
|
|
- primaryKey: true,
|
|
|
- type: Sequelize.DataTypes.INTEGER
|
|
|
- },
|
|
|
- menuItemId: {
|
|
|
- type: Sequelize.DataTypes.INTEGER,
|
|
|
- allowNull: false,
|
|
|
- comment: 'для внешнего ключа'
|
|
|
- },
|
|
|
- quantity: {
|
|
|
- type: Sequelize.DataTypes.INTEGER,
|
|
|
- allowNull: false,
|
|
|
- comment: 'количество'
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- /**
|
|
|
- * Внешний ключ корзина_блюдо
|
|
|
- */
|
|
|
- await queryInterface.addConstraint('Cart', {
|
|
|
- fields: ['menuItemId'],
|
|
|
- type: 'foreign key',
|
|
|
- name: 'FK_cart_menu-item',
|
|
|
- references: {
|
|
|
- table: 'MenuItem',
|
|
|
- field: 'id'
|
|
|
- },
|
|
|
- onDelete: 'no action',
|
|
|
- onUpdate: 'no action'
|
|
|
- })
|
|
|
-
|
|
|
- },
|
|
|
-
|
|
|
- async down (queryInterface, Sequelize) {
|
|
|
- await queryInterface.removeConstraint('Cart', 'FK_cart_menu-item')
|
|
|
- await queryInterface.dropTable('Cart')
|
|
|
- }
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-Накатываем миграцию командой `npx sequelize-cli db:migrate` и у нас в схеме бд появится таблица Cart:
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-### CRUD для корзины
|
|
|
-
|
|
|
-Для того, чтобы тело запроса автоматически преобразовывалось в объект _body_ нужно после создания экземпляра приложения подключить _middleware_ `express.json()` (_middleware_ это функции, которые выполняются до обработки _endpoints_, используются для служебных целей, как в нашем случае, и в целях авторизации. Позже добавим jwt-авторизацию и напишем для неё _middleware_):
|
|
|
-
|
|
|
-```js
|
|
|
-...
|
|
|
-const app = express()
|
|
|
-app.use(express.json())
|
|
|
-...
|
|
|
-```
|
|
|
-
|
|
|
-#### Create. Для добавления записей в таблицу используется метод POST
|
|
|
-
|
|
|
-```js
|
|
|
-app.post('/api/cart', async function(req, res) {
|
|
|
- try {
|
|
|
- await sequelize.query(`
|
|
|
- INSERT INTO Cart (menuItemId, quantity)
|
|
|
- -- значения :menuItemId и :quantity задаются в replacements
|
|
|
- VALUES (:menuItemId, :quantity)
|
|
|
- `,{
|
|
|
- logging: false,
|
|
|
- type: QueryTypes.INSERT,
|
|
|
- // объект replacements должен содержать значения для замены
|
|
|
- replacements: {
|
|
|
- menuItemId: req.body.menuItemId,
|
|
|
- quantity: req.body.quantity
|
|
|
- }
|
|
|
- })
|
|
|
- // ничего не возвращаем, но код ответа меняем на 201 (Created)
|
|
|
- // можно и не менять, по-умолчанию возвращает 200 (OK)
|
|
|
- res.status(201)
|
|
|
- } catch (error) {
|
|
|
- console.warn('ошибка при добавлении блюда в корзину:', error.message)
|
|
|
- // при ошибке возвращаем код ошибки 500 и текст ошибки
|
|
|
- res.status(500).send(error.message)
|
|
|
- } finally {
|
|
|
- res.end()
|
|
|
- }
|
|
|
-})
|
|
|
-```
|
|
|
-
|
|
|
-#### Read. Чтение
|
|
|
-
|
|
|
-Используется метод get. Пример приводить не буду, он аналогичен запросу списка блюд
|
|
|
-
|
|
|
-#### Update. Редактирование корзины
|
|
|
-
|
|
|
-Для редактирования в REST используются методы PUT или PATCH. PUT - полная перезапись (замещение объекта), PATCH - частичная перезапись (изменение части объекта)
|
|
|
-
|
|
|
-Мы будем только менять количество, поэтому используем PATCH
|
|
|
-
|
|
|
-```js
|
|
|
-// в пути можно описать параметр (поставив знак ":")
|
|
|
-app.patch('/api/cart/:id', async (req, res) => {
|
|
|
- try {
|
|
|
- await sequelize.query(`
|
|
|
- UPDATE Cart
|
|
|
- SET quantity=:quantity
|
|
|
- WHERE id=:id
|
|
|
- `,{
|
|
|
- logging: false,
|
|
|
- replacements: {
|
|
|
- // используем параметр из пути
|
|
|
- id: req.params.id,
|
|
|
- quantity: req.body.quantity
|
|
|
- }
|
|
|
- })
|
|
|
- } catch (error) {
|
|
|
- console.warn('ошибка при редактировании корзины:', error.message)
|
|
|
- res.status(500).send(error.message)
|
|
|
- } finally {
|
|
|
- res.end()
|
|
|
- }
|
|
|
-})
|
|
|
-```
|
|
|
-
|
|
|
-#### Delete. Удаление блюда из корзины
|
|
|
-
|
|
|
-Тут ничего нового
|
|
|
-
|
|
|
-```js
|
|
|
-app.delete('/api/cart/:id', async (req, res) => {
|
|
|
- try {
|
|
|
- await sequelize.query(`
|
|
|
- DELETE
|
|
|
- FROM Cart
|
|
|
- WHERE id=:id
|
|
|
- `,{
|
|
|
- logging: false,
|
|
|
- replacements: {
|
|
|
- id: req.params.id
|
|
|
- }
|
|
|
- })
|
|
|
- } catch (error) {
|
|
|
- console.warn('ошибка при удалении блюда из корзины:', error.message)
|
|
|
- res.status(500).send(error.message)
|
|
|
- } finally {
|
|
|
- res.end()
|
|
|
- }
|
|
|
-})
|
|
|
-```
|
|
|
-
|
|
|
----
|
|
|
-
|
|
|
-## JWT-авторизация
|
|
|
-
|
|
|
->JSON Web Token (JWT) — это JSON объект, который определен в открытом стандарте RFC 7519. Он считается одним из безопасных способов передачи информации между двумя участниками. Для его создания необходимо определить заголовок (header) с общей информацией по токену, полезные данные (payload), такие как id пользователя, его роль и т.д. и подписи (signature).
|
|
|
-
|
|
|
-### Структура JWT
|
|
|
-
|
|
|
-JWT состоит из трех частей: заголовок `header`, полезные данные `payload` и подпись `signature`. Давайте пройдемся по каждой из них.
|
|
|
-
|
|
|
-
|
|
|
-#### Шаг 1. Создаем HEADER
|
|
|
-
|
|
|
-Хедер JWT содержит информацию о том, как должна вычисляться JWT подпись. Хедер — это тоже JSON объект, который выглядит следующим образом:
|
|
|
-
|
|
|
-```js
|
|
|
-header = { "alg": "HS256", "typ": "JWT"}
|
|
|
-```
|
|
|
-
|
|
|
-Поле `typ` не говорит нам ничего нового, только то, что это _JSON Web Token_. Интереснее здесь будет поле `alg`, которое определяет алгоритм хеширования. Он будет использоваться при создании подписи. HS256 — не что иное, как HMAC-SHA256, для его вычисления нужен лишь один секретный ключ (более подробно об этом в шаге 3). Еще может использоваться другой алгоритм RS256 — в отличие от предыдущего, он является ассиметричным и создает два ключа: публичный и приватный. С помощью приватного ключа создается подпись, а с помощью публичного только лишь проверяется подлинность подписи, поэтому нам не нужно беспокоиться о его безопасности.
|
|
|
-
|
|
|
-
|
|
|
-#### Шаг 2. Создаем PAYLOAD
|
|
|
-
|
|
|
-`Payload` — это полезные данные, которые хранятся внутри **JWT**. Эти данные также называют JWT-claims (заявки). В примере, который рассматриваем мы, сервер аутентификации создает **JWT** с информацией об `id` пользователя — `userId`.
|
|
|
-
|
|
|
-```js
|
|
|
-payload = { "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.
|
|
|
-
|
|
|
-### Как JWT защищает наши данные?
|
|
|
-
|
|
|
-Очень важно понимать, что использование **JWT** НЕ скрывает и не маскирует данные. Причина, почему **JWT** используются — это проверка, что отправленные данные были действительно отправлены авторизованным источником. Как было продемонстрировано выше, данные внутри **JWT** закодированы и подписаны, обратите внимание, это не одно и тоже, что зашифрованы. Цель кодирования данных — преобразование структуры. Подписанные данные позволяют получателю данных проверить аутентификацию источника данных. Таким образом закодирование и подпись данных не защищает их. С другой стороны, главная цель шифрования — это защита данных от неавторизованного доступа.
|
|
|
-
|
|
|
-### Простыми словами
|
|
|
-
|
|
|
-1. При авторизации по логину/паролю сервер убеждается, что такой пользователь есть, формирует **JWT**-токен, содержащий полезную информацию и подпись, и возвращает этот токен клиенту (сайт, приложение)
|
|
|
-
|
|
|
-1. Клиент при последующих запрсах добавляет **JWT**-токен в заголовок запроса:
|
|
|
-
|
|
|
- ```
|
|
|
- Authorization: Bearer <JWT-токен>
|
|
|
- ```
|
|
|
-
|
|
|
-1. Сервер, получая **JWT**-токен уже не лезет в таблицу пользователей, а просто проверяет подпись токена, убеждаясь что она не подделана (ключ подписи есть только у севрера). А затем может использовать полезную информацию из токена в запросах к БД. Таким образом мы экономим на каждом входящем запросе одно обращение к БД (а работа с БД самое медленное действие, вычисление подписи намного дешевле)
|
|
|
-
|
|
|
-### Подключаем 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
|
|
|
-/**
|
|
|
- * В теле запроса должен быть объект с логином и паролем:
|
|
|
- * {
|
|
|
- * "login": "ваш логин",
|
|
|
- * "password": "пароль"
|
|
|
- * }
|
|
|
- */
|
|
|
-app.post('/api/user/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('пользователь не найден')
|
|
|
+ })
|
|
|
+
|
|
|
+ // Пример создания таблицы связей (многие-ко-многим)
|
|
|
+ // особенность в том, что первичный ключ в таких таблицах составной
|
|
|
+ // и создаётся отдельно
|
|
|
+ await queryInterface.createTable('NewsRole', {
|
|
|
+ newsId: {
|
|
|
+ type: Sequelize.DataTypes.INTEGER,
|
|
|
+ allowNull: false
|
|
|
+ },
|
|
|
+ roleId: {
|
|
|
+ type: Sequelize.DataTypes.INTEGER,
|
|
|
+ allowNull: false
|
|
|
}
|
|
|
- } catch (error) {
|
|
|
- console.warn('ошибка при авторизации:', error.message)
|
|
|
- res.status(500).send(error.message)
|
|
|
- } finally {
|
|
|
- res.end()
|
|
|
- }
|
|
|
-})
|
|
|
-```
|
|
|
+ })
|
|
|
|
|
|
-#### Реализуем аутентификацию пользователя в запросе корзины, используя middleware
|
|
|
+ // добавление первичного ключа
|
|
|
+ await queryInterface.addConstraint('NewsRole', {
|
|
|
+ fields: ['newsId', 'roleId'],
|
|
|
+ type: 'primary key',
|
|
|
+ name: 'PK_NewsRole'
|
|
|
+ })
|
|
|
|
|
|
-**Middleware** функция принимает на входе параметры `req`, `res`, `next`, где `req`, `res` соответственно запрос и ответ, а `next` нужно вызвать, если всё нормально и можно вызывать следующую middleware функцию (их может быть несколько)
|
|
|
+ // далее создаются внешние ключи для полей newsId и roleId
|
|
|
|
|
|
-```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
|
|
|
-/**
|
|
|
- * Вторым параметром запроса можно добавить массив middleware
|
|
|
- */
|
|
|
-app.get('/api/cart', [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()
|
|
|
- }
|
|
|
-})
|
|
|
-```
|
|
|
-
|
|
|
## Задание
|
|
|
|
|
|
-Реализовать АПИ для своего курсового проекта по образцу. В следующем году постараюсь написать лекции по DevOps (завернём АПИ, базу и приложение в контейнеры).
|
|
|
+- Добавить в проект пакеты для работы с базой данных
|
|
|
+- настройте `sequelize`
|
|
|
+- создайте базу данных
|
|
|
+- создайте миграцию с командами создания таблиц и связей между ними (заполните словари, если они есть)
|
|
|
+
|
|
|
+ Этот пункт делать только после утверждения ERD
|