Аутентификация и авторизация с JWT в Express.js

Введение В этой статье мы поговорим о том, как работают веб-токены JSON, каковы их преимущества, их структура и способы их использования для обработки базовой аутентификации и авторизации в Express. Вам не обязательно иметь опыт работы с веб-токенами JSON, поскольку мы будем говорить об этом с нуля. Для раздела реализации было бы предпочтительнее, если у вас есть предыдущий опыт работы с Express [https://expressjs.com/], Javascript ES6 и клиентами REST. Какие

Вступление

В этой статье мы поговорим о том, как работают JSON Web Tokens, каковы их преимущества, их структура и как их использовать для обработки базовой аутентификации и авторизации в Express.

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

Для раздела реализации было бы предпочтительнее, если у вас есть предыдущий опыт работы с Express , Javascript ES6 и клиентами REST.

Что такое веб-токены JSON?

Веб-токены JSON (JWT) были введены как метод безопасного обмена данными между двумя сторонами. Он был представлен со спецификацией RFC 7519 Инженерной группой Интернета (IETF).

Несмотря на то, что мы можем использовать JWT с любым типом метода связи, сегодня JWT очень популярен для обработки аутентификации и авторизации через HTTP.

Во-первых, вам нужно знать несколько характеристик HTTP.

HTTP - это протокол без сохранения состояния, что означает, что HTTP-запрос не поддерживает состояние. Сервер не знает ни о каких предыдущих запросах, отправленных тем же клиентом.

HTTP-запросы должны быть автономными. Они должны включать информацию о предыдущих запросах, сделанных пользователем, в самом запросе.

Есть несколько способов сделать это, однако самый популярный способ - установить идентификатор сеанса , который является ссылкой на информацию о пользователе.

Сервер сохранит этот идентификатор сеанса в памяти или в базе данных. Клиент будет отправлять каждый запрос с этим идентификатором сеанса. Затем сервер может получить информацию о клиенте, используя эту ссылку.

Вот схема того, как работает аутентификация на основе сеанса:

session_based_authentication{.ezlazyload}

Обычно этот идентификатор сеанса отправляется пользователю в виде файла cookie. Мы уже подробно обсуждали это в нашей предыдущей статье « Обработка аутентификации в Express.js» .

С другой стороны, с JWT, когда клиент отправляет запрос аутентификации на сервер, он отправляет токен JSON обратно клиенту, который включает в себя всю информацию о пользователе с ответом.

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

Например, предположим, что после аутентификации сервер отправляет обратно клиенту объект JSON с именем пользователя и сроком действия. Итак, поскольку объект JSON доступен для чтения, любой может изменить эту информацию и отправить запрос. Проблема в том, что нет возможности подтвердить такой запрос.

Именно здесь вступает в силу подписание токена. Таким образом, вместо того, чтобы просто отправлять обратно простой токен JSON, сервер отправляет подписанный токен, который может проверить, что информация не изменилась.

Мы рассмотрим это более подробно позже в этой статье.

Вот схема того, как работает JWT:

json_web_tokens{.ezlazyload}

Структура JWT

Давайте поговорим о структуре JWT на примере токена:

sample_json_web_token_jwt{.ezlazyload}

Как вы можете видеть на изображении, JWT состоит из трех частей, каждая из которых разделена точкой.

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

Первый раздел JWT - это заголовок, представляющий собой строку в кодировке Base64. Если вы расшифруете заголовок, он будет выглядеть примерно так:

 { 
 "alg": "HS256", 
 "typ": "JWT" 
 } 

Раздел заголовка содержит алгоритм хеширования, который использовался для генерации знака и типа токена.

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

Рекомендуется не включать в JWT какие-либо конфиденциальные данные, такие как пароли или личную информацию.

Обычно тело JWT выглядит примерно так, хотя это необязательно:

 { 
 "sub": "1234567890", 
 "name": "John Doe", 
 "iat": 1516239022 
 } 

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

Вы также можете увидеть некоторые общие свойства, такие как eat или exp , который является сроком действия токена.

Последний раздел - это подпись токена. Это создается путем хеширования строки base64UrlEncode(header) + "." + base64UrlEncode(payload) + secret с использованием алгоритма, упомянутого в разделе заголовка.

secret - это случайная строка, которую должен знать только сервер. Никакой хеш не может быть преобразован обратно в исходный текст, и даже небольшое изменение исходной строки приведет к другому хешу. Таким образом, secret не может быть реконструирован.

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

Согласно стандартам, клиент должен отправить этот токен на сервер через HTTP-запрос в заголовке под названием Authorization с формой Bearer [JWT_TOKEN] . Таким образом, значение Authorization будет выглядеть примерно так:

 Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o 

Если вы хотите узнать больше о структуре токена JWT, вы можете ознакомиться с нашей подробной статьей Understanding JSON Web Tokens . Вы также можете посетить jwt.io и поиграть с их отладчиком:

jwt_debugger{.ezlazyload}

Преимущество использования JWT перед традиционными методами

Как мы обсуждали ранее, JWT может содержать всю информацию о самом пользователе, в отличие от аутентификации на основе сеанса.

Это очень полезно для масштабирования веб-приложений, например веб-приложений с микросервисами. Сегодня архитектура современного веб-приложения выглядит примерно так:

web_application_architecture{.ezlazyload}

Все эти службы могут быть одной и той же службой, которая будет перенаправлена балансировщиком нагрузки в соответствии с использованием ресурсов (ЦП или использование памяти) каждого сервера или некоторыми различными службами, такими как аутентификация и т. Д.

Если мы используем традиционные методы авторизации, такие как файлы cookie, нам придется совместно использовать базу данных, такую как Redis , для обмена сложной информацией между серверами или внутренними службами. Но если мы поделимся секретом между микросервисами, мы можем просто использовать JWT, и тогда для авторизации пользователей не потребуется никаких других внешних ресурсов.

Использование JWT с Express

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

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

  • только просматривать их. В идеале они также могли бы иметь возможность редактировать или удалять книги. Но чтобы статья была как можно проще, мы не будем вдаваться в подробности.

Для начала в вашем терминале инициализируйте пустой проект Node.js с настройками по умолчанию:

 $ npm init -y 

Затем давайте установим фреймворк Express:

 $ npm install --save express 

Служба аутентификации

Затем давайте создадим файл auth.js , который будет нашей службой аутентификации:

 const express = require('express'); 
 const app = express(); 
 
 app.listen(3000, () => { 
 console.log('Authentication service started on port 3000'); 
 }); 

В идеале мы должны использовать базу данных для хранения информации о пользователях. Но для простоты давайте создадим массив пользователей, который мы будем использовать для их аутентификации.

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

 const users = [ 
 { 
 username: 'john', 
 password: 'password123admin', 
 role: 'admin' 
 }, { 
 username: 'anna', 
 password: 'password123member', 
 role: 'member' 
 } 
 ]; 

Теперь мы можем создать обработчик запроса для входа пользователя. Давайте установим модуль jsonwebtoken , который используется для генерации и проверки токенов JWT.

Также давайте установим body-parser для анализа тела JSON из HTTP-запроса:

 $ npm i --save body-parser jsonwebtoken 

Теперь давайте эти модули и настроим их в приложении Express:

 const jwt = require('jsonwebtoken'); 
 const bodyParser = require('body-parser'); 
 
 app.use(bodyParser.json()); 

Теперь мы можем создать обработчик запроса для обработки запроса входа пользователя:

 const accessTokenSecret = 'youraccesstokensecret'; 

Это ваш секрет для подписи токена JWT. Вы никогда не должны делиться этим секретом, иначе злоумышленник может использовать его для подделки токенов JWT для получения несанкционированного доступа к вашему сервису. Чем сложнее этот токен доступа, тем безопаснее будет ваше приложение. Поэтому попробуйте использовать для этого токена сложную случайную строку:

 app.post('/login', (req, res) => { 
 // Read username and password from request body 
 const { username, password } = req.body; 
 
 // Filter user from the users array by username and password 
 const user = users.find(u => { return u.username === username && u.password === password }); 
 
 if (user) { 
 // Generate an access token 
 const accessToken = jwt.sign({ username: user.username, role: user.role }, accessTokenSecret); 
 
 res.json({ 
 accessToken 
 }); 
 } else { 
 res.send('Username or password incorrect'); 
 } 
 }); 

В этом обработчике мы искали пользователя, который соответствует имени пользователя и паролю в теле запроса. Затем мы сгенерировали токен доступа с объектом JSON с именем пользователя и ролью пользователя.

Наша служба аутентификации готова. Загрузим его, запустив:

 $ node auth.js 

После того, как служба аутентификации будет запущена, давайте отправим запрос POST и посмотрим, работает ли он.

Для этого я воспользуюсь клиентом отдыха Insomnia . Не стесняйтесь использовать для этого любой клиент отдыха или что-то вроде Postman .

Давайте отправим почтовый запрос в http://localhost:3000/login со следующим JSON:

 { 
 "username": "john", 
 "password": "password123admin" 
 } 

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

 { 
 "accessToken": "eyJhbGciOiJIUz..." 
 } 

insomnia_access{.ezlazyload}

Книжная служба

После этого давайте создадим books.js для нашего книжного сервиса.

Мы начнем с файла, импортировав необходимые библиотеки и настроив приложение Express:

 const express = require('express'); 
 const bodyParser = require('body-parser'); 
 const jwt = require('jsonwebtoken'); 
 
 const app = express(); 
 
 app.use(bodyParser.json()); 
 
 app.listen(4000, () => { 
 console.log('Books service started on port 4000'); 
 }); 

После настройки, чтобы смоделировать базу данных, давайте просто создадим массив книг:

 const books = [ 
 { 
 "author": "Chinua Achebe", 
 "country": "Nigeria", 
 "language": "English", 
 "pages": 209, 
 "title": "Things Fall Apart", 
 "year": 1958 
 }, 
 { 
 "author": "Hans Christian Andersen", 
 "country": "Denmark", 
 "language": "Danish", 
 "pages": 784, 
 "title": "Fairy tales", 
 "year": 1836 
 }, 
 { 
 "author": "Dante Alighieri", 
 "country": "Italy", 
 "language": "Italian", 
 "pages": 928, 
 "title": "The Divine Comedy", 
 "year": 1315 
 }, 
 ]; 

Теперь мы можем создать очень простой обработчик запросов для извлечения всех книг из базы данных:

 app.get('/books', (req, res) => { 
 res.json(books); 
 }); 

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

Перед этим создайте секрет токена доступа для подписи JWT, как и раньше:

 const accessTokenSecret = 'youraccesstokensecret'; 

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

На этом этапе давайте создадим промежуточное ПО Express, которое обрабатывает процесс аутентификации:

 const authenticateJWT = (req, res, next) => { 
 const authHeader = req.headers.authorization; 
 
 if (authHeader) { 
 const token = authHeader.split(' ')[1]; 
 
 jwt.verify(token, accessTokenSecret, (err, user) => { 
 if (err) { 
 return res.sendStatus(403); 
 } 
 
 req.user = user; 
 next(); 
 }); 
 } else { 
 res.sendStatus(401); 
 } 
 }; 

В этом промежуточном программном обеспечении мы читаем значение заголовка авторизации. Поскольку authorization имеет значение в формате Bearer [JWT_TOKEN] , мы разделили значение пробелом и разделили токен.

Затем мы проверили токен с помощью JWT. После проверки мы присоединяем user к запросу и продолжаем. В противном случае мы отправим клиенту ошибку.

Мы можем настроить это промежуточное ПО в нашем обработчике запросов GET, например:

 app.get('/books', authenticateJWT, (req, res) => { 
 res.json(books); 
 }); 

Давайте загрузим сервер и проверим, все ли работает правильно:

 $ node books.js 

Теперь мы можем отправить запрос на http://localhost:4000/books чтобы получить все книги из базы данных.

Убедитесь, что вы изменили заголовок «Authorization», чтобы он содержал значение «Bearer [JWT_TOKEN]», как показано на изображении ниже:

insomnia_get_books{.ezlazyload}

Наконец, мы можем создать наш обработчик запросов для создания книги. Поскольку только admin может добавить новую книгу, в этом обработчике мы также должны проверить роль пользователя.

Мы также можем использовать промежуточное ПО для аутентификации, которое мы использовали выше:

 app.post('/books', authenticateJWT, (req, res) => { 
 const { role } = req.user; 
 
 if (role !== 'admin') { 
 return res.sendStatus(403); 
 } 
 
 
 const book = req.body; 
 books.push(book); 
 
 res.send('Book added successfully'); 
 }); 

Поскольку промежуточное программное обеспечение аутентификации связывает пользователя с запросом, мы можем получить role из req.user и просто проверить, является ли пользователь admin . Если да, то книга добавляется, в противном случае выдается ошибка.

Давайте попробуем это с нашим клиентом REST. Войдите в систему как admin (используя тот же метод, что и выше), а затем скопируйте accessToken и отправьте его с Authorization как мы сделали в предыдущем примере.

Затем мы можем отправить запрос POST в конечную точку http://localhost:4000/books

 { 
 "author": "Jane Austen", 
 "country": "United Kingdom", 
 "language": "English", 
 "pages": 226, 
 "title": "Pride and Prejudice", 
 "year": 1813 
 } 

insomnia_add_book{.ezlazyload}

Обновление токена

На данный момент наше приложение обрабатывает как аутентификацию, так и авторизацию для книжного сервиса, хотя в конструкции есть серьезный недостаток - токен JWT никогда не истекает.

Если этот токен будет украден, у них будет доступ к учетной записи навсегда, и фактический пользователь не сможет отозвать доступ.

Чтобы исключить эту возможность, давайте обновим наш обработчик запросов на вход, чтобы срок действия токена истекал через определенный период. Мы можем сделать это, передав expiresIn в качестве опции для подписи JWT.

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

Сначала создайте секрет токена обновления и пустой массив для хранения токенов обновления:

 const refreshTokenSecret = 'yourrefreshtokensecrethere'; 
 const refreshTokens = []; 

Когда пользователь входит в систему, вместо создания одного токена генерируйте токены обновления и аутентификации:

 app.post('/login', (req, res) => { 
 // read username and password from request body 
 const { username, password } = req.body; 
 
 // filter user from the users array by username and password 
 const user = users.find(u => { return u.username === username && u.password === password }); 
 
 if (user) { 
 // generate an access token 
 const accessToken = jwt.sign({ username: user.username, role: user.role }, accessTokenSecret, { expiresIn: '20m' }); 
 const refreshToken = jwt.sign({ username: user.username, role: user.role }, refreshTokenSecret); 
 
 refreshTokens.push(refreshToken); 
 
 res.json({ 
 accessToken, 
 refreshToken 
 }); 
 } else { 
 res.send('Username or password incorrect'); 
 } 
 }); 

А теперь давайте создадим обработчик запросов, который генерирует новые токены на основе токенов обновления:

 app.post('/token', (req, res) => { 
 const { token } = req.body; 
 
 if (!token) { 
 return res.sendStatus(401); 
 } 
 
 if (!refreshTokens.includes(token)) { 
 return res.sendStatus(403); 
 } 
 
 jwt.verify(token, refreshTokenSecret, (err, user) => { 
 if (err) { 
 return res.sendStatus(403); 
 } 
 
 const accessToken = jwt.sign({ username: user.username, role: user.role }, accessTokenSecret, { expiresIn: '20m' }); 
 
 res.json({ 
 accessToken 
 }); 
 }); 
 }); 

Но и с этим есть проблема. Если токен обновления украден у пользователя, кто-то может использовать его для создания любого количества новых токенов.

Чтобы этого избежать, реализуем простую функцию logout

 app.post('/logout', (req, res) => { 
 const { token } = req.body; 
 refreshTokens = refreshTokens.filter(token => t !== token); 
 
 res.send("Logout successful"); 
 }); 

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

Заключение

В этой статье мы познакомили вас с JWT и как реализовать JWT с Express. Я надеюсь, что теперь у вас есть хорошие знания о том, как работает JWT и как реализовать его в своем проекте.

Как всегда, исходный код доступен на GitHub .

comments powered by Disqus