|
@@ -13,6 +13,7 @@
|
|
|
* [Использование sequelize для получения данных из БД](#использование-sequelize-для-получения-данных-из-бд)
|
|
* [Использование sequelize для получения данных из БД](#использование-sequelize-для-получения-данных-из-бд)
|
|
|
* [Создание скрипта для наполнения БД начальными данными](#создание-скрипта-для-наполнения-бд-начальными-данными)
|
|
* [Создание скрипта для наполнения БД начальными данными](#создание-скрипта-для-наполнения-бд-начальными-данными)
|
|
|
* [Добавление сущностей БД, реализация REST.](#добавление-сущностей-бд-реализация-rest)
|
|
* [Добавление сущностей БД, реализация REST.](#добавление-сущностей-бд-реализация-rest)
|
|
|
|
|
+* [JWT-авторизация](#jwt-авторизация)
|
|
|
|
|
|
|
|
## Создание проекта
|
|
## Создание проекта
|
|
|
|
|
|
|
@@ -563,6 +564,243 @@ app.delete('/api/cart/:id', async (req, res) => {
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
|
|
+## 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('пользователь не найден')
|
|
|
|
|
+ }
|
|
|
|
|
+ } 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
|
|
|
|
|
+/**
|
|
|
|
|
+ * Вторым параметром запроса можно добавить массив 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 (завернём АПИ, базу и приложение в контейнеры).
|
|
Реализовать АПИ для своего курсового проекта по образцу. В следующем году постараюсь написать лекции по DevOps (завернём АПИ, базу и приложение в контейнеры).
|