17. JWT аутентификация

JWT (JSON Web Token) — стандарт для передачи данных между клиентом и сервером в виде подписанного токена. Основа аутентификации в REST API.
Структура JWT
Заголовок раздела «Структура JWT»eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJ1c2VyIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ↑ Header ↑ Payload ↑ Signature
Header: { "alg": "HS256", "typ": "JWT" }Payload: { "userId": 1, "role": "user", "iat": 1234567890, "exp": 1234654290 }Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)npm install jsonwebtoken bcryptjsСоздание и проверка токенов
Заголовок раздела «Создание и проверка токенов»const jwt = require('jsonwebtoken');const { JWT_SECRET, JWT_EXPIRES_IN } = require('./config');
// Создание токенаfunction createToken(payload) { return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN || '7d', // algorithm: 'HS256', // по умолчанию });}
// Проверка токенаfunction verifyToken(token) { try { return jwt.verify(token, JWT_SECRET); // Возвращает payload: { userId: 1, role: 'user', iat: ..., exp: ... } } catch (err) { if (err.name === 'TokenExpiredError') throw new Error('Токен истёк'); if (err.name === 'JsonWebTokenError') throw new Error('Неверный токен'); throw err; }}
// Пример использованияconst token = createToken({ userId: 42, role: 'admin' });const payload = verifyToken(token);console.log(payload.userId); // 42Хэширование паролей
Заголовок раздела «Хэширование паролей»const bcrypt = require('bcryptjs');
// Хэширование (при регистрации)async function hashPassword(password) { const saltRounds = 12; // чем больше — тем безопаснее и медленнее return bcrypt.hash(password, saltRounds);}
// Проверка пароля (при входе)async function comparePassword(password, hash) { return bcrypt.compare(password, hash);}
// Использованиеconst hash = await hashPassword('mySecretPassword123');console.log(hash); // $2a$12$...
const isValid = await comparePassword('mySecretPassword123', hash);console.log(isValid); // trueПолная реализация Auth
Заголовок раздела «Полная реализация Auth»const { Router } = require('express');const bcrypt = require('bcryptjs');const jwt = require('jsonwebtoken');const { z } = require('zod');const { validate } = require('../middleware/validate');const { authenticate } = require('../middleware/auth');const { db } = require('../db');const config = require('../config');
const router = Router();
const registerSchema = z.object({ name: z.string().min(2).max(50), email: z.string().email().toLowerCase(), password: z.string().min(8).max(72) .regex(/[A-Z]/, 'Минимум одна заглавная буква') .regex(/[0-9]/, 'Минимум одна цифра'),});
const loginSchema = z.object({ email: z.string().email().toLowerCase(), password: z.string().min(1),});
// Регистрацияrouter.post('/register', validate(registerSchema), async (req, res, next) => { try { const { name, email, password } = req.body;
// Проверяем уникальность email const existing = await db.user.findUnique({ where: { email } }); if (existing) { return res.status(409).json({ error: 'Email уже зарегистрирован' }); }
// Хэшируем пароль const passwordHash = await bcrypt.hash(password, 12);
// Создаём пользователя const user = await db.user.create({ data: { name, email, passwordHash }, select: { id: true, name: true, email: true, createdAt: true }, });
// Создаём токен const token = jwt.sign( { userId: user.id, role: 'user' }, config.jwt.secret, { expiresIn: config.jwt.expiresIn } );
res.status(201).json({ message: 'Аккаунт создан!', token, user, }); } catch (err) { next(err); } });
// Входrouter.post('/login', validate(loginSchema), async (req, res, next) => { try { const { email, password } = req.body;
// Находим пользователя const user = await db.user.findUnique({ where: { email }, select: { id: true, name: true, email: true, passwordHash: true, role: true }, });
// Одинаковое сообщение для безопасности! const errorMsg = 'Неверный email или пароль'; if (!user) return res.status(401).json({ error: errorMsg });
// Проверяем пароль const isValid = await bcrypt.compare(password, user.passwordHash); if (!isValid) return res.status(401).json({ error: errorMsg });
// Токен const token = jwt.sign( { userId: user.id, role: user.role }, config.jwt.secret, { expiresIn: config.jwt.expiresIn } );
const { passwordHash: _, ...userData } = user; res.json({ token, user: userData }); } catch (err) { next(err); } });
// Текущий пользовательrouter.get('/me', authenticate, async (req, res, next) => { try { const user = await db.user.findUnique({ where: { id: req.user.userId }, select: { id: true, name: true, email: true, role: true, createdAt: true }, }); res.json({ data: user }); } catch (err) { next(err); }});
module.exports = router;Refresh Tokens
Заголовок раздела «Refresh Tokens»// Access token — короткоживущий (15 мин)// Refresh token — долгоживущий (30 дней), хранится в httpOnly cookie
router.post('/refresh', async (req, res, next) => { try { const refreshToken = req.cookies.refreshToken; if (!refreshToken) { return res.status(401).json({ error: 'Refresh токен не найден' }); }
// Проверяем refresh токен const payload = jwt.verify(refreshToken, config.jwt.refreshSecret);
// Проверяем в БД (чтобы можно было отозвать) const stored = await db.refreshToken.findFirst({ where: { token: refreshToken, userId: payload.userId }, }); if (!stored) { return res.status(401).json({ error: 'Токен отозван' }); }
// Создаём новый access token const accessToken = jwt.sign( { userId: payload.userId, role: payload.role }, config.jwt.secret, { expiresIn: '15m' } );
res.json({ accessToken }); } catch (err) { if (err.name === 'TokenExpiredError') { return res.status(401).json({ error: 'Refresh токен истёк, войдите заново' }); } next(err); }});
// Выход — инвалидируем refresh токенrouter.post('/logout', authenticate, async (req, res) => { const refreshToken = req.cookies.refreshToken; if (refreshToken) { await db.refreshToken.deleteMany({ where: { token: refreshToken } }); } res.clearCookie('refreshToken'); res.json({ message: 'Вышли из системы' });});Middleware аутентификации
Заголовок раздела «Middleware аутентификации»const jwt = require('jsonwebtoken');const config = require('../config');
function authenticate(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) { return res.status(401).json({ error: 'Токен не предоставлен' }); }
try { const token = authHeader.slice(7); req.user = jwt.verify(token, config.jwt.secret); next(); } catch (err) { const msg = err.name === 'TokenExpiredError' ? 'Токен истёк' : 'Неверный токен'; res.status(401).json({ error: msg }); }}
// Опциональная авторизация — продолжает даже без токенаfunction optionalAuth(req, res, next) { const authHeader = req.headers.authorization; if (authHeader?.startsWith('Bearer ')) { try { req.user = jwt.verify(authHeader.slice(7), config.jwt.secret); } catch {} // игнорируем ошибки } next();}
module.exports = { authenticate, optionalAuth };Практика
Заголовок раздела «Практика»- Реализуй регистрацию с bcrypt хэшированием пароля
- Реализуй вход с генерацией JWT токена
- Создай middleware
authenticateи защити им несколько роутов - Добавь эндпоинт
GET /auth/meдля получения текущего пользователя - Реализуй систему refresh токенов (access 15 мин + refresh 30 дней в httpOnly cookie)