Вступление
В этой статье мы поговорим о том, как работают JSON Web Tokens, каковы их преимущества, их структура и как их использовать для обработки базовой аутентификации и авторизации в Express.
Вам не обязательно иметь опыт работы с веб-токенами JSON, поскольку мы будем говорить об этом с нуля.
Для раздела реализации было бы предпочтительнее, если у вас есть предыдущий опыт работы с Express , Javascript ES6 и клиентами REST.
Что такое веб-токены JSON?
Веб-токены JSON (JWT) были введены как метод безопасного обмена данными между двумя сторонами. Он был представлен со спецификацией RFC 7519 Инженерной группой Интернета (IETF).
Несмотря на то, что мы можем использовать JWT с любым типом метода связи, сегодня JWT очень популярен для обработки аутентификации и авторизации через HTTP.
Во-первых, вам нужно знать несколько характеристик HTTP.
HTTP - это протокол без сохранения состояния, что означает, что HTTP-запрос не поддерживает состояние. Сервер не знает ни о каких предыдущих запросах, отправленных тем же клиентом.
HTTP-запросы должны быть автономными. Они должны включать информацию о предыдущих запросах, сделанных пользователем, в самом запросе.
Есть несколько способов сделать это, однако самый популярный способ - установить идентификатор сеанса , который является ссылкой на информацию о пользователе.
Сервер сохранит этот идентификатор сеанса в памяти или в базе данных. Клиент будет отправлять каждый запрос с этим идентификатором сеанса. Затем сервер может получить информацию о клиенте, используя эту ссылку.
Вот схема того, как работает аутентификация на основе сеанса:
{.ezlazyload}
Обычно этот идентификатор сеанса отправляется пользователю в виде файла cookie. Мы уже подробно обсуждали это в нашей предыдущей статье « Обработка аутентификации в Express.js» .
С другой стороны, с JWT, когда клиент отправляет запрос аутентификации на сервер, он отправляет токен JSON обратно клиенту, который включает в себя всю информацию о пользователе с ответом.
Клиент отправит этот токен вместе со всеми последующими запросами. Таким образом, серверу не нужно будет хранить какую-либо информацию о сеансе. Но у такого подхода есть проблема. Кто угодно может отправить поддельный запрос с поддельным токеном JSON и притвориться кем-то, кем он не является.
Например, предположим, что после аутентификации сервер отправляет обратно клиенту объект JSON с именем пользователя и сроком действия. Итак, поскольку объект JSON доступен для чтения, любой может изменить эту информацию и отправить запрос. Проблема в том, что нет возможности подтвердить такой запрос.
Именно здесь вступает в силу подписание токена. Таким образом, вместо того, чтобы просто отправлять обратно простой токен JSON, сервер отправляет подписанный токен, который может проверить, что информация не изменилась.
Мы рассмотрим это более подробно позже в этой статье.
Вот схема того, как работает JWT:
{.ezlazyload}
Структура JWT
Давайте поговорим о структуре 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 и поиграть с их отладчиком:
{.ezlazyload}
Преимущество использования JWT перед традиционными методами
Как мы обсуждали ранее, JWT может содержать всю информацию о самом пользователе, в отличие от аутентификации на основе сеанса.
Это очень полезно для масштабирования веб-приложений, например веб-приложений с микросервисами. Сегодня архитектура современного веб-приложения выглядит примерно так:
{.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..."
}
{.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]», как показано на изображении ниже:
{.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
}
{.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 .