Защита REST API в Node.js с помощью JWT

Когда вы создаёте приложение, важно защитить конфиденциальные данные от неавторизованного доступа. Современные web, мобильные и облачные приложения часто используют REST API как основной канал взаимодействия. Поэтому проектирование и разработка backend API должны учитывать безопасность с самого начала.
JSON Web Tokens (JWT) — эффективный способ обеспечить авторизацию и аутентификацию пользователей, а также защитить ресурсы от доступа злоумышленников.
Что такое JWT
JSON Web Token (JWT) — это открытый стандарт для компактной и автономной передачи информации между сторонами в виде JSON-объекта, подписанного цифровой подписью. JWT обычно используется для передачи данных о пользователе и проверки подлинности.
JWT состоит из трёх частей, разделённых точками: заголовок (header), полезная нагрузка (payload) и подпись (signature). Заголовок описывает алгоритм подписи (например, HS256), payload содержит утверждения (claims) о пользователе и метаданные, а подпись гарантирует целостность и подлинность токена.
Коротко: сервер подписывает payload секретом — клиент получает токен и при каждом запросе предоставляет его серверу для проверки.
Цели статьи
- Показать простую реализацию регистрации и входа с использованием JWT в Node.js и Express.
- Объяснить, как защищать маршруты с помощью middleware, проверяющего токен.
- Дать практические советы по безопасности, тестированию и эксплуатации.
Пример приложения: стек и установка
Мы построим минимальный API с регистрацией, логином и защищённым маршрутом для получения списка пользователей.
Рекомендуемые зависимости (установите в корневой папке проекта):
npm install cors dotenv bcrypt mongoose cookie-parser crypto jsonwebtoken expressПримечание: мы используем mongoose для работы с MongoDB и bcrypt для хеширования паролей.
Создайте базу данных MongoDB (локально или кластер в облаке) и в корне проекта файл .env с подключением:
CONNECTION_STRING="ваша_строка_подключения"Настройка подключения к базе данных
Создайте файл utils/db.js и добавьте код для подключения через mongoose:
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.CONNECTION_STRING, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log("Connected to MongoDB!");
} catch (error) {
console.error("Error connecting to MongoDB:", error);
}
};
module.exports = connectDB;Краткое определение: Mongoose — ODM (Object Document Mapper), упрощающий работу с MongoDB.
Модель данных пользователя
Создайте папку models и файл models/user.model.js с простым описанием пользователя:
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true }
});
const User = mongoose.model("User", userSchema);
module.exports = User;Важно: username помечен как unique — это облегчает предотвращение дублирования учётных записей.
Контроллеры: регистрация, вход и защищённый маршрут
Создайте папку controllers и файл controllers/userControllers.js. Ниже — пример реализации регистрационного и логин-контроллеров, а также обработчика защищённого маршрута.
const User = require('../models/user.model');
const bcrypt = require('bcrypt');
const { generateToken } = require('../middleware/auth');
exports.registerUser = async (req, res) => {
const { username, password } = req.body;
try {
const hash = await bcrypt.hash(password, 10);
await User.create({ username, password: hash });
res.status(201).send({ message: 'User registered successfully' });
} catch (error) {
console.log(error);
res.status(500).send({ message: 'An error occurred' });
}
};
exports.loginUser = async (req, res) => {
const { username, password } = req.body;
try {
const user = await User.findOne({ username });
if (!user) {
return res.status(404).send({ message: 'User not found' });
}
const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) {
return res.status(401).send({ message: 'Invalid login credentials' });
}
const payload = { userId: user._id };
const token = generateToken(payload);
res.cookie('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production' });
res.status(200).json({ message: 'Login successful' });
} catch (error) {
console.log(error);
res.status(500).send({ message: 'An error occurred while logging in' });
}
};
exports.getUsers = async (req, res) => {
try {
const users = await User.find({});
res.json(users);
} catch (error) {
console.log(error);
res.status(500).send({ message: 'An error occurred' });
}
};Примечание: токен возвращается в httpOnly cookie — это защищает от доступа токена через JavaScript (XSS).
Middleware аутентификации и конфигурация
Создайте папку middleware и два файла: middleware/config.js и middleware/auth.js.
middleware/config.js:
const crypto = require('crypto');
module.exports = {
// Для реального приложения храните ключ в надёжном месте (например, в секретах облака)
secretKey: process.env.JWT_SECRET || crypto.randomBytes(32).toString('hex')
};Important: генерация нового секрета при каждом запуске приведёт к инвалидизации всех ранее выданных токенов. В продакшне храните JWT_SECRET явно (в переменных окружения или хранилище секретов).
middleware/auth.js:
const jwt = require('jsonwebtoken');
const { secretKey } = require('./config');
const generateToken = (payload) => {
const token = jwt.sign(payload, secretKey, { expiresIn: '1h' });
return token;
};
const verifyToken = (req, res, next) => {
const token = req.cookies && req.cookies.token;
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
jwt.verify(token, secretKey, (err, decoded) => {
if (err) {
return res.status(401).json({ message: 'Invalid or expired token' });
}
req.userId = decoded.userId;
next();
});
};
module.exports = { generateToken, verifyToken };Ключевые моменты: ограничение срока жизни токена (expiresIn) и проверка подписи помогут снизить риск использования перехваченных токенов.
Маршруты API
Создайте routes/userRoutes.js:
const express = require('express');
const router = express.Router();
const userControllers = require('../controllers/userControllers');
const { verifyToken } = require('../middleware/auth');
router.post('/api/register', userControllers.registerUser);
router.post('/api/login', userControllers.loginUser);
router.get('/api/users', verifyToken, userControllers.getUsers);
module.exports = router;Точка входа сервера
Пример server.js:
const express = require('express');
const cors = require('cors');
const app = express();
const port = process.env.PORT || 5000;
require('dotenv').config();
const connectDB = require('./utils/db');
const cookieParser = require('cookie-parser');
connectDB();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors({ credentials: true, origin: true }));
app.use(cookieParser());
const userRoutes = require('./routes/userRoutes');
app.use('/', userRoutes);
app.listen(port, () => {
console.log(`Server is listening at http://localhost:${port}`);
});Запуск сервера в разработке:
node server.jsТестирование: используйте Postman, curl или интеграционные тесты для запросов к /api/register, /api/login и /api/users. При успешном логине cookie с токеном автоматически отправляется клиенту и включается в последующие запросы.
Практические рекомендации по безопасности
JWT — мощный инструмент, но он не закрывает все проблемы. Ниже — набор практик для повышения безопасности API.
- Используйте HTTPS повсеместно. Токены и учётные данные должны передаваться только по TLS.
- Храните секреты (JWT_SECRET) вне кода: переменные окружения, секретное хранилище в облаке.
- Устанавливайте короткий срок жизни токена и используйте механизм обновления (refresh tokens) с безопасным хранением и возможностью отзыва.
- Ограничивайте доступ по ролям: добавляйте в payload роль или список прав и проверяйте их в middleware.
- Проводите валидацию и санитизацию входных данных (например, express-validator).
- Ограничивайте скорость запросов (rate limiting) для защиты от брутфорса.
- Логируйте события безопасности (входы, неудачные попытки) и отслеживайте аномалии.
- При хранении токена в cookie: ставьте httpOnly, secure и SameSite=strict/ lax в зависимости от архитектуры.
Когда JWT не лучшее решение (контрпримеры)
- Если нужно мгновенно отзывать токены у большого числа пользователей: стандартный JWT без сессий и централизованного черного списка сложнее отзывать. Решение: храните список отозванных идентификаторов токенов или используйте короткий срок жизни + refresh tokens.
- Если вы не контролируете домен клиента (встраиваемый виджет), хранение токенов в localStorage уязвимо к XSS.
- Для очень чувствительных операций (финансы) имеет смысл применять двуфакторную аутентификацию и дополнительные проверки.
Альтернативы и гибридные подходы
- Серверные сессии (session ID в cookie) — проще отзывать и контролировать, но требует серверного хранилища сессий.
- OAuth2 + OpenID Connect — стандарт для авторизации через сторонние провайдеры и делегирования прав.
- Комбинация JWT (access token) и refresh token — распространённый баланс между удобством и безопасностью.
Модель принятия решений (когда что выбрать)
- Нужна масштабируемая stateless аутентификация → JWT с коротким сроком жизни + refresh tokens.
- Требуется быстрый отзыв доступа и контроль сессий → серверные сессии или централизованный blacklist для JWT.
- Нужно делегировать аутентификацию внешнему провайдеру → OAuth2/OIDC.
Чек-лист ролей (разработчик / DevOps / SRE)
Разработчик:
- Хеширование паролей (bcrypt) — выполнено.
- Валидация входных данных на всех маршрутах.
- Ограничение прав на маршрутах по ролям.
DevOps:
- Хранение JWT_SECRET в безопасном хранилище.
- TLS для всех внешних подключений.
- Настройка резервирования и мониторинга БД.
SRE / Безопасность:
- Настройка rate limiting и WAF.
- Политики логирования и оповещений по подозрительным событиям.
- План отзыва ключей и ротации секретов.
Тесты и критерии приёмки
- Регистрация: POST /api/register создаёт пользователя с захешированным паролем.
- Вход: POST /api/login возвращает httpOnly cookie с валидным JWT.
- Защищённый маршрут: GET /api/users возвращает 401 без токена, 200 с валидным токеном.
- Тесты на истечение срока действия токена и некорректную подпись.
Пример сценария инцидента и откат
- Выявлено, что секретный ключ JWT скомпрометирован.
- Откат: немедленно сменить JWT_SECRET, увеличить логирование неудачных попыток.
- Инвалидировать токены: либо централизованный blacklist, либо заставить пользователей повторно пройти аутентификацию (например, уменьшить срок жизни токенов и не выдавать refresh tokens).
- Проанализировать ошибку и восстановить процессы выпуска ключей.
Краткая сводка мер безопасности (матрица рисков)
- Утечка токена через XSS — риск: высокий; смягчение: httpOnly cookie, CSP, защита от XSS.
- Перехват токена в сети — риск: высокий; смягчение: HTTPS.
- Кража секретного ключа — риск: критичный; смягчение: хранение секретов в Vault, ротация ключей, мониторинг.
Локальные и практические советы для развёртывания
- В продакшне не генерируйте секрет ключ на старте — используйте управляемые секреты.
- Если фронтенд и бэкенд на разных доменах, настройте CORS с credentials:true и выставьте SameSite для cookie корректно.
- Для мобильных клиентов рассмотрите безопасное хранилище токенов (Keychain/Keystore).
Глоссарий (в 1 строку)
- JWT — подписьованный JSON-токен для передачи утверждений о пользователе.
- Access token — токен с коротким сроком жизни для доступа к ресурсам.
- Refresh token — более долгоживущий токен для получения новых access token.
Итог
JWT упрощают реализацию stateless аутентификации и хорошо подходят для масштабируемых REST API. Однако сами по себе они не решают всех задач безопасности — необходим комплексный подход: HTTPS, хранение секретов, валидация входа, ограничение прав, мониторинг и возможность отзыва токенов. Следуя описанным в статье шаблонам, вы получите рабочую и более безопасную отправную точку для авторизации в Node.js приложении.
Важно: прежде чем использовать JWT в продакшне, оцените требования к отзыву сессий и хранению секретов — это ключевые архитектурные решения.
Похожие материалы
Как настроить домашний медиасервер
Установка macOS на ПК — подробный гид
Открыть Локальные пользователи и группы — Windows 11
Продвинутый поиск в LinkedIn — тактики и шаблоны
Как очистить корзину на Android