JSON Web Tokens (JWT): Полное руководство по аутентификации
Исчерпывающее руководство по JWT: структура, подпись, аутентификация, безопасность и практические примеры реализации.
Big JSON Team
• Technical WriterExpert in JSON data manipulation, API development, and web technologies. Passionate about creating tools that make developers' lives easier.
# JSON Web Tokens (JWT): Полное руководство по аутентификации
JSON Web Tokens (JWT) — это открытый стандарт (RFC 7519) для безопасной передачи информации между сторонами в виде JSON объекта. В этом руководстве мы разберем, как работает JWT и как правильно его использовать.
Что такое JWT?
JWT — это компактный, URL-безопасный способ представления claims (утверждений) для передачи между двумя сторонами.
Основные характеристики
- Самодостаточный: Токен содержит всю необходимую информацию о пользователе
- Компактный: Может передаваться через URL, POST параметр или HTTP заголовок
- Безопасный: Подписан или зашифрован
Когда использовать JWT?
Структура JWT
JWT состоит из трех частей, разделенных точками:
Header.Payload.Signature
Визуальный пример
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ItCY0LLQsNC9IiwiYWRtaW4iOnRydWV9
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1. Header (Заголовок)
Содержит метаданные о токене:
{
"alg": "HS256",
"typ": "JWT"
}
- alg: Алгоритм подписи (HS256, RS256, и т.д.)
- typ: Тип токена (всегда JWT)
Затем header кодируется в Base64URL.
2. Payload (Полезная нагрузка)
Содержит claims — утверждения о пользователе:
{
"sub": "1234567890",
"name": "Иван",
"admin": true,
"iat": 1516239022,
"exp": 1516242622
}
Типы claims:
Registered (Зарегистрированные):
iss(issuer): Издатель токенаsub(subject): Субъект (ID пользователя)aud(audience): Аудиторияexp(expiration): Время истеченияnbf(not before): Не использовать доiat(issued at): Время созданияjti(JWT ID): Уникальный ID токена
Определяются пользователем, но должны быть зарегистрированы в IANA JWT Registry.
Private (Частные):Пользовательские claims для обмена информацией.
3. Signature (Подпись)
Гарантирует целостность токена:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Создание JWT
Node.js с jsonwebtoken
const jwt = require('jsonwebtoken');
// Секретный ключ (храните в переменных окружения!)
const СЕКРЕТ = process.env.JWT_SECRET || 'ваш-супер-секретный-ключ';
// Создание токена
function создатьТокен(пользователь) {
const payload = {
sub: пользователь.id,
name: пользователь.имя,
email: пользователь.email,
роль: пользователь.роль
};
const опции = {
expiresIn: '24h', // Токен истекает через 24 часа
issuer: 'ваше-приложение',
audience: 'пользователи-приложения'
};
return jwt.sign(payload, СЕКРЕТ, опции);
}
// Использование
const пользователь = {
id: 123,
имя: 'Иван Петров',
email: 'ivan@example.com',
роль: 'admin'
};
const токен = создатьТокен(пользователь);
console.log('JWT:', токен);
Python с PyJWT
import jwt
import datetime
import os
# Секретный ключ
СЕКРЕТ = os.getenv('JWT_SECRET', 'ваш-супер-секретный-ключ')
def создать_токен(пользователь):
"""
Создание JWT токена
"""
payload = {
'sub': пользователь['id'],
'name': пользователь['имя'],
'email': пользователь['email'],
'роль': пользователь['роль'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24),
'iat': datetime.datetime.utcnow(),
'iss': 'ваше-приложение'
}
токен = jwt.encode(payload, СЕКРЕТ, algorithm='HS256')
return токен
# Использование
пользователь = {
'id': 123,
'имя': 'Иван Петров',
'email': 'ivan@example.com',
'роль': 'admin'
}
токен = создать_токен(пользователь)
print(f'JWT: {токен}')
Проверка JWT
Node.js
const jwt = require('jsonwebtoken');
function проверитьТокен(токен) {
try {
const payload = jwt.verify(токен, СЕКРЕТ, {
issuer: 'ваше-приложение',
audience: 'пользователи-приложения'
});
console.log('Токен валиден');
console.log('Пользователь:', payload.name);
console.log('Email:', payload.email);
return payload;
} catch (err) {
if (err.name === 'TokenExpiredError') {
console.error('Токен истек');
} else if (err.name === 'JsonWebTokenError') {
console.error('Невалидный токен');
} else {
console.error('Ошибка проверки:', err.message);
}
return null;
}
}
// Использование
const токен = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const payload = проверитьТокен(токен);
if (payload) {
console.log('Доступ разрешен');
}
Python
import jwt
def проверить_токен(токен):
"""
Проверка JWT токена
"""
try:
payload = jwt.decode(
токен,
СЕКРЕТ,
algorithms=['HS256'],
issuer='ваше-приложение'
)
print('Токен валиден')
print(f"Пользователь: {payload['name']}")
print(f"Email: {payload['email']}")
return payload
except jwt.ExpiredSignatureError:
print('Токен истек')
return None
except jwt.InvalidTokenError as e:
print(f'Невалидный токен: {e}')
return None
# Использование
токен = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
payload = проверить_токен(токен)
if payload:
print('Доступ разрешен')
Middleware для Express
Базовая защита роутов
const jwt = require('jsonwebtoken');
// Middleware для проверки JWT
function аутентификация(req, res, next) {
// Получить токен из заголовка
const authHeader = req.headers['authorization'];
const токен = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!токен) {
return res.status(401).json({
ошибка: 'Токен не предоставлен'
});
}
try {
const payload = jwt.verify(токен, СЕКРЕТ);
req.пользователь = payload; // Добавить данные пользователя в request
next();
} catch (err) {
return res.status(403).json({
ошибка: 'Невалидный или истекший токен'
});
}
}
// Использование в роутах
const express = require('express');
const app = express();
// Публичный роут
app.get('/api/публичные', (req, res) => {
res.json({ сообщение: 'Публичные данные' });
});
// Защищенный роут
app.get('/api/профиль', аутентификация, (req, res) => {
res.json({
пользователь: req.пользователь.name,
email: req.пользователь.email
});
});
// Роут только для админов
function толькоАдмин(req, res, next) {
if (req.пользователь.роль !== 'admin') {
return res.status(403).json({
ошибка: 'Требуются права администратора'
});
}
next();
}
app.get('/api/админ', аутентификация, толькоАдмин, (req, res) => {
res.json({ сообщение: 'Админ панель' });
});
Продвинутый middleware
function создатьАутентификациюMiddleware(опции = {}) {
const {
секрет = process.env.JWT_SECRET,
получитьТокен = стандартноеПолучениеТокена,
обработкаОшибки = стандартнаяОбработкаОшибки
} = опции;
return function(req, res, next) {
const токен = получитьТокен(req);
if (!токен) {
return обработкаОшибки(req, res, 'no_token');
}
jwt.verify(токен, секрет, (err, payload) => {
if (err) {
return обработкаОшибки(req, res, err.name);
}
req.пользователь = payload;
next();
});
};
}
function стандартноеПолучениеТокена(req) {
// Из заголовка Authorization
const authHeader = req.headers['authorization'];
if (authHeader) {
return authHeader.split(' ')[1];
}
// Из cookie
if (req.cookies && req.cookies.токен) {
return req.cookies.токен;
}
// Из query параметра
if (req.query.токен) {
return req.query.токен;
}
return null;
}
function стандартнаяОбработкаОшибки(req, res, ошибка) {
const статус = ошибка === 'no_token' ? 401 : 403;
const сообщения = {
'no_token': 'Токен не предоставлен',
'TokenExpiredError': 'Токен истек',
'JsonWebTokenError': 'Невалидный токен'
};
res.status(статус).json({
ошибка: сообщения[ошибка] || 'Ошибка аутентификации'
});
}
// Использование
const аутентификация = создатьАутентификациюMiddleware();
app.use('/api/защищенные', аутентификация);
Flask (Python)
Базовая защита
from flask import Flask, request, jsonify
from functools import wraps
import jwt
app = Flask(__name__)
def требуется_токен(f):
"""
Декоратор для защиты роутов
"""
@wraps(f)
def decorated(args, kwargs):
токен = None
# Получить токен из заголовка
if 'Authorization' in request.headers:
auth_header = request.headers['Authorization']
токен = auth_header.split(' ')[1] if ' ' in auth_header else None
if not токен:
return jsonify({'ошибка': 'Токен не предоставлен'}), 401
try:
payload = jwt.decode(токен, СЕКРЕТ, algorithms=['HS256'])
request.пользователь = payload
except jwt.ExpiredSignatureError:
return jsonify({'ошибка': 'Токен истек'}), 403
except jwt.InvalidTokenError:
return jsonify({'ошибка': 'Невалидный токен'}), 403
return f(args, *kwargs)
return decorated
# Публичный роут
@app.route('/api/публичные')
def публичные():
return jsonify({'сообщение': 'Публичные данные'})
# Защищенный роут
@app.route('/api/профиль')
@требуется_токен
def профиль():
return jsonify({
'пользователь': request.пользователь['name'],
'email': request.пользователь['email']
})
# Роут только для админов
def только_админ(f):
@wraps(f)
def decorated(args, *kwargs):
if request.пользователь.get('роль') != 'admin':
return jsonify({'ошибка': 'Требуются права администратора'}), 403
return f(args, kwargs)
return decorated
@app.route('/api/админ')
@требуется_токен
@только_админ
def админ():
return jsonify({'сообщение': 'Админ панель'})
Refresh Tokens
Концепция
- Access Token: Короткоживущий (15-30 минут)
- Refresh Token: Долгоживущий (7-30 дней)
Реализация
function генерироватьТокены(пользователь) {
// Access token - короткий
const accessToken = jwt.sign(
{
sub: пользователь.id,
name: пользователь.имя,
роль: пользователь.роль
},
СЕКРЕТ,
{ expiresIn: '15m' }
);
// Refresh token - длинный
const refreshToken = jwt.sign(
{
sub: пользователь.id,
тип: 'refresh'
},
REFRESH_СЕКРЕТ,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// Роут для обновления токена
app.post('/api/обновить', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ ошибка: 'Refresh токен отсутствует' });
}
try {
const payload = jwt.verify(refreshToken, REFRESH_СЕКРЕТ);
if (payload.тип !== 'refresh') {
throw new Error('Неверный тип токена');
}
// Получить пользователя из БД
const пользователь = получитьПользователяПоID(payload.sub);
// Сгенерировать новые токены
const токены = генерироватьТокены(пользователь);
res.json(токены);
} catch (err) {
res.status(403).json({ ошибка: 'Невалидный refresh токен' });
}
});
Алгоритмы подписи
HMAC (Симметричный)
// HS256, HS384, HS512
const токен = jwt.sign(payload, 'секретный-ключ', {
algorithm: 'HS256'
});
// Тот же ключ для проверки
jwt.verify(токен, 'секретный-ключ');
Плюсы:
- Быстрый
- Простой
Минусы:
- Один ключ для подписи и проверки
- Ключ должен быть секретным
RSA (Асимметричный)
const fs = require('fs');
// Приватный ключ для подписи
const privateKey = fs.readFileSync('private.pem');
const токен = jwt.sign(payload, privateKey, {
algorithm: 'RS256'
});
// Публичный ключ для проверки
const publicKey = fs.readFileSync('public.pem');
jwt.verify(токен, publicKey);
Плюсы:
- Публичный ключ можно распространять
- Только приватный ключ может подписывать
Минусы:
- Медленнее HMAC
- Сложнее управление ключами
Генерация RSA ключей
# OpenSSL
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
Безопасность JWT
1. Храните секреты безопасно
❌ Плохо:
const СЕКРЕТ = 'мой-секрет';
✅ Хорошо:
const СЕКРЕТ = process.env.JWT_SECRET;
if (!СЕКРЕТ) {
throw new Error('JWT_SECRET не установлен');
}
2. Используйте короткое время жизни
// Access token - короткий
jwt.sign(payload, секрет, { expiresIn: '15m' });
// Refresh token - длинный
jwt.sign(payload, refresh_секрет, { expiresIn: '7d' });
3. Проверяйте все claims
jwt.verify(токен, секрет, {
algorithms: ['HS256'], // Только разрешенные алгоритмы
issuer: 'ваше-приложение',
audience: 'пользователи'
});
4. Не храните чувствительные данные
❌ Плохо:
const payload = {
пароль: 'секрет123',
номер_карты: '1234-5678-9012-3456'
};
✅ Хорошо:
const payload = {
sub: пользователь.id,
роль: пользователь.роль
};
5. Реализуйте blacklist для отзыва
const redis = require('redis');
const client = redis.createClient();
async function отозватьТокен(токен) {
const payload = jwt.decode(токен);
const ttl = payload.exp - Math.floor(Date.now() / 1000);
// Добавить в blacklist
await client.setex(blacklist:${токен}, ttl, 'true');
}
async function проверитьBlacklist(токен) {
const blacklisted = await client.get(blacklist:${токен});
return blacklisted !== null;
}
// В middleware
async function аутентификация(req, res, next) {
const токен = получитьТокен(req);
if (await проверитьBlacklist(токен)) {
return res.status(403).json({ ошибка: 'Токен отозван' });
}
// ... обычная проверка ...
}
6. Защита от атак
CSRF Protection:
// Не полагайтесь только на cookies
// Используйте Authorization заголовок
res.setHeader('Authorization', Bearer ${токен});
XSS Protection:
// Не храните токены в localStorage
// Используйте httpOnly cookies
res.cookie('токен', токен, {
httpOnly: true,
secure: true, // Только HTTPS
sameSite: 'strict'
});
Тестирование
Unit тесты
const jwt = require('jsonwebtoken');
const { expect } = require('chai');
describe('JWT функции', () => {
const ТЕСТОВЫЙ_СЕКРЕТ = 'тестовый-секрет';
it('должен создать валидный токен', () => {
const payload = { sub: 123, name: 'Тест' };
const токен = jwt.sign(payload, ТЕСТОВЫЙ_СЕКРЕТ);
expect(токен).to.be.a('string');
expect(токен.split('.')).to.have.lengthOf(3);
});
it('должен проверить токен', () => {
const payload = { sub: 123, name: 'Тест' };
const токен = jwt.sign(payload, ТЕСТОВЫЙ_СЕКРЕТ);
const decoded = jwt.verify(токен, ТЕСТОВЫЙ_СЕКРЕТ);
expect(decoded.sub).to.equal(123);
expect(decoded.name).to.equal('Тест');
});
it('должен отклонить истекший токен', (done) => {
const токен = jwt.sign(
{ sub: 123 },
ТЕСТОВЫЙ_СЕКРЕТ,
{ expiresIn: '1ms' }
);
setTimeout(() => {
try {
jwt.verify(токен, ТЕСТОВЫЙ_СЕКРЕТ);
done(new Error('Должна была быть ошибка'));
} catch (err) {
expect(err.name).to.equal('TokenExpiredError');
done();
}
}, 10);
});
});
Отладка JWT
Декодирование токена
// Декодировать без проверки подписи
const decoded = jwt.decode(токен, { complete: true });
console.log('Header:', decoded.header);
console.log('Payload:', decoded.payload);
console.log('Signature:', decoded.signature);
Онлайн инструменты
- jwt.io: Декодирование и отладка токенов
- jwt-debugger*: Проверка подписи
Логирование
function логироватьТокен(токен) {
const decoded = jwt.decode(токен);
console.log('Токен информация:');
console.log(' Пользователь:', decoded.sub);
console.log(' Издан:', new Date(decoded.iat 1000).toLocaleString());
console.log(' Истекает:', new Date(decoded.exp * 1000).toLocaleString());
console.log(' Осталось:', Math.floor((decoded.exp - Date.now() / 1000) / 60), 'минут');
}
Заключение
JWT — мощный инструмент для аутентификации и авторизации в современных приложениях. Ключевые выводы:
✅ Используйте:
- Короткое время жизни access токенов
- Refresh токены для продления сессии
- Безопасное хранение секретов
- HTTPS для передачи токенов
❌ Избегайте:
- Хранения чувствительных данных в payload
- Длинного времени жизни access токенов
- Слабых секретных ключей
- Хранения токенов в localStorage (XSS риск)
JWT — это не серебряная пуля, но при правильном использовании он обеспечивает надежную и масштабируемую систему аутентификации!
Похожие статьи
Python и JSON: Полное руководство по работе с данными
Исчерпывающее руководство по работе с JSON в Python: парсинг, сериализация, обработка ошибок, продвинутые техники и best practices.
JavaScript и JSON: Полное руководство для разработчиков
Комплексное руководство по работе с JSON в JavaScript: встроенные методы, парсинг, сериализация, обработка ошибок и продвинутые техники.
JSON API и REST сервисы: Полное руководство
Изучите использование JSON в RESTful API, лучшие практики проектирования, аутентификацию и обработку ошибок.