- Home »

Node.js JWT с Express.js — руководство по аутентификации
Аутентификация в современных веб-приложениях — это не просто “введите логин и пароль”. Это целая экосистема безопасности, где каждая деталь важна. JWT (JSON Web Token) стал де-факто стандартом для stateless аутентификации, а в связке с Express.js превращается в мощный инструмент разработчика. Если вы запускаете API на собственном сервере, настраиваете микросервисы или просто хотите понять, как работает современная аутентификация “под капотом” — эта статья для вас.
Мы разберём не только базовую реализацию, но и продвинутые техники: refresh tokens, middleware для защиты роутов, обработка ошибок и интеграция с базами данных. А главное — покажем, как всё это развернуть на реальном сервере и не наступить на грабли, которые поджидают каждого разработчика.
Как работает JWT аутентификация
JWT — это не магия, а простая и элегантная система. Токен состоит из трёх частей, разделённых точками: header.payload.signature. Каждая часть закодирована в base64, а подпись создаётся с помощью секретного ключа.
Принцип работы прост: клиент отправляет credentials, сервер проверяет их и возвращает JWT. Далее клиент включает этот токен в заголовок Authorization каждого запроса. Сервер декодирует токен, проверяет подпись и извлекает данные пользователя.
Преимущества JWT | Недостатки JWT |
---|---|
Stateless (не требует хранения на сервере) | Нельзя отозвать до истечения срока |
Масштабируемость | Размер токена больше session ID |
Кроссдоменность | Уязвимость к XSS атакам |
Мобильная совместимость | Сложность в отладке |
Быстрая настройка проекта
Начнём с создания нового проекта и установки зависимостей:
mkdir jwt-auth-app
cd jwt-auth-app
npm init -y
npm install express jsonwebtoken bcryptjs cors helmet dotenv
npm install --save-dev nodemon
Создаём базовую структуру файлов:
mkdir routes middleware models
touch app.js .env
touch routes/auth.js routes/protected.js
touch middleware/auth.js
touch models/user.js
Настраиваем основное приложение в app.js:
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
require('dotenv').config();
const authRoutes = require('./routes/auth');
const protectedRoutes = require('./routes/protected');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/protected', protectedRoutes);
// Error handling
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Создание системы аутентификации
Файл .env с настройками:
JWT_SECRET=your-super-secret-key-change-this-in-production
JWT_EXPIRES_IN=1h
JWT_REFRESH_SECRET=your-refresh-secret-key
JWT_REFRESH_EXPIRES_IN=7d
BCRYPT_ROUNDS=12
Создаём модель пользователя (models/user.js):
const bcrypt = require('bcryptjs');
// Простая in-memory база для примера
const users = [];
class User {
constructor(username, email, password) {
this.id = Date.now().toString();
this.username = username;
this.email = email;
this.password = password;
this.createdAt = new Date();
}
static async create(userData) {
const { username, email, password } = userData;
// Проверяем, существует ли пользователь
const existingUser = users.find(u => u.email === email);
if (existingUser) {
throw new Error('User already exists');
}
// Хешируем пароль
const hashedPassword = await bcrypt.hash(password, parseInt(process.env.BCRYPT_ROUNDS));
const user = new User(username, email, hashedPassword);
users.push(user);
return user;
}
static async findByEmail(email) {
return users.find(u => u.email === email);
}
static async findById(id) {
return users.find(u => u.id === id);
}
async comparePassword(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
}
}
module.exports = User;
Middleware для проверки токенов (middleware/auth.js):
const jwt = require('jsonwebtoken');
const User = require('../models/user');
const authMiddleware = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
req.user = user;
next();
} catch (error) {
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'Invalid token' });
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
res.status(500).json({ error: 'Server error' });
}
};
module.exports = authMiddleware;
Роуты аутентификации
Создаём routes/auth.js с полным набором эндпоинтов:
const express = require('express');
const jwt = require('jsonwebtoken');
const User = require('../models/user');
const authMiddleware = require('../middleware/auth');
const router = express.Router();
// Генерация токенов
const generateTokens = (userId) => {
const accessToken = jwt.sign(
{ id: userId },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN }
);
const refreshToken = jwt.sign(
{ id: userId },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN }
);
return { accessToken, refreshToken };
};
// Регистрация
router.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;
// Валидация
if (!username || !email || !password) {
return res.status(400).json({ error: 'All fields are required' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Password must be at least 6 characters' });
}
const user = await User.create({ username, email, password });
const tokens = generateTokens(user.id);
res.status(201).json({
message: 'User created successfully',
user: {
id: user.id,
username: user.username,
email: user.email
},
...tokens
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Вход
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password are required' });
}
const user = await User.findByEmail(email);
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const tokens = generateTokens(user.id);
res.json({
message: 'Login successful',
user: {
id: user.id,
username: user.username,
email: user.email
},
...tokens
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Обновление токена
router.post('/refresh', async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
const tokens = generateTokens(user.id);
res.json(tokens);
} catch (error) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
// Получение информации о пользователе
router.get('/me', authMiddleware, (req, res) => {
res.json({
user: {
id: req.user.id,
username: req.user.username,
email: req.user.email,
createdAt: req.user.createdAt
}
});
});
module.exports = router;
Защищённые роуты
Создаём routes/protected.js для демонстрации защищённых эндпоинтов:
const express = require('express');
const authMiddleware = require('../middleware/auth');
const router = express.Router();
// Все роуты в этом файле требуют аутентификации
router.use(authMiddleware);
router.get('/dashboard', (req, res) => {
res.json({
message: 'Welcome to your dashboard!',
user: {
id: req.user.id,
username: req.user.username
},
timestamp: new Date().toISOString()
});
});
router.get('/profile', (req, res) => {
res.json({
profile: {
id: req.user.id,
username: req.user.username,
email: req.user.email,
createdAt: req.user.createdAt
}
});
});
module.exports = router;
Тестирование API
Запускаем сервер:
npm run dev
Добавляем в package.json:
{
"scripts": {
"dev": "nodemon app.js",
"start": "node app.js"
}
}
Тестируем с помощью curl:
# Регистрация
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"john","email":"john@example.com","password":"password123"}'
# Вход
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"john@example.com","password":"password123"}'
# Доступ к защищённому роуту
curl -X GET http://localhost:3000/api/protected/dashboard \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Продвинутые техники и оптимизация
Для продакшена добавляем дополнительные слои безопасности:
const rateLimit = require('express-rate-limit');
// Rate limiting для auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 5, // максимум 5 попыток
message: 'Too many authentication attempts'
});
router.use('/login', authLimiter);
Middleware для логирования:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'auth.log' })
]
});
// Используем в middleware
logger.info('User login attempt', { email, ip: req.ip });
Альтернативные решения
Решение | Преимущества | Недостатки |
---|---|---|
Passport.js | Множество стратегий, готовые решения | Сложность настройки, overhead |
Auth0 | Готовый сервис, безопасность | Стоимость, зависимость от внешнего сервиса |
Firebase Auth | Интеграция с Google, простота | Vendor lock-in, ограничения |
Sessions + Redis | Возможность отзыва, традиционность | Stateful, проблемы с масштабированием |
Развёртывание на сервере
Для развёртывания на VPS создаём ecosystem.config.js для PM2:
module.exports = {
apps: [{
name: 'jwt-auth-app',
script: 'app.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true
}]
};
Настройка nginx для reverse proxy:
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
Интеграция с базами данных
Для подключения MongoDB:
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
refreshTokens: [{ type: String }], // Для хранения refresh токенов
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('User', userSchema);
Для PostgreSQL с Sequelize:
const { DataTypes } = require('sequelize');
const User = sequelize.define('User', {
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false
}
});
Автоматизация и скрипты
Создаём полезные скрипты для управления:
// scripts/generateSecret.js
const crypto = require('crypto');
const generateSecret = () => {
return crypto.randomBytes(64).toString('hex');
};
console.log('JWT_SECRET=' + generateSecret());
console.log('JWT_REFRESH_SECRET=' + generateSecret());
// scripts/cleanupTokens.js
const User = require('../models/user');
async function cleanupExpiredTokens() {
// Логика очистки истёкших refresh токенов
const users = await User.find({});
for (const user of users) {
user.refreshTokens = user.refreshTokens.filter(token => {
try {
jwt.verify(token, process.env.JWT_REFRESH_SECRET);
return true;
} catch {
return false;
}
});
await user.save();
}
}
// Запуск через cron
cleanupExpiredTokens().catch(console.error);
Мониторинг и аналитика
Добавляем middleware для отслеживания:
const analytics = {
loginAttempts: 0,
successfulLogins: 0,
failedLogins: 0,
activeUsers: new Set()
};
const trackAuth = (req, res, next) => {
const originalSend = res.send;
res.send = function(data) {
if (req.path === '/login') {
analytics.loginAttempts++;
if (res.statusCode === 200) {
analytics.successfulLogins++;
} else {
analytics.failedLogins++;
}
}
return originalSend.call(this, data);
};
next();
};
// Эндпоинт для получения статистики
router.get('/stats', (req, res) => {
res.json({
loginAttempts: analytics.loginAttempts,
successfulLogins: analytics.successfulLogins,
failedLogins: analytics.failedLogins,
activeUsers: analytics.activeUsers.size
});
});
Безопасность в продакшене
Важные моменты для безопасности:
- Используйте HTTPS всегда
- Храните секретные ключи в переменных окружения
- Добавьте rate limiting для критичных эндпоинтов
- Валидируйте все входные данные
- Логируйте подозрительную активность
- Используйте короткие времена жизни для access токенов
- Реализуйте blacklist для отозванных токенов
Заключение и рекомендации
JWT аутентификация с Express.js — это мощный и гибкий подход для современных веб-приложений. Она идеально подходит для API, микросервисов и SPA приложений. Главные преимущества: stateless природа, масштабируемость и простота интеграции.
Рекомендую использовать JWT когда:
- Вы строите API для мобильных приложений
- Нужна аутентификация между микросервисами
- Планируете горизонтальное масштабирование
- Работаете с SPA фреймворками
Для высоконагруженных проектов рассмотрите выделенный сервер с Redis для кеширования и балансировкой нагрузки.
Избегайте JWT если:
- Нужна возможность мгновенного отзыва доступа
- Работаете с чувствительными данными в токене
- Приложение имеет сложную систему ролей
Помните: безопасность — это процесс, а не результат. Регулярно обновляйте зависимости, мониторьте логи и следите за best practices. JWT — это инструмент, а не панацея.
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.