Перейти к содержимому

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

Иллюстрация к уроку

JWT (JSON Web Token) — стандарт для передачи данных между клиентом и сервером в виде подписанного токена. Основа аутентификации в REST API.

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
routes/auth.js
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;
// 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/auth.js
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 };
  1. Реализуй регистрацию с bcrypt хэшированием пароля
  2. Реализуй вход с генерацией JWT токена
  3. Создай middleware authenticate и защити им несколько роутов
  4. Добавь эндпоинт GET /auth/me для получения текущего пользователя
  5. Реализуй систему refresh токенов (access 15 мин + refresh 30 дней в httpOnly cookie)