Node.jsМодуль 6: Безопасность
Аутентификация
Sessions, Cookies, JWT токены
Цель урока
В этом уроке ты научишься:
- Различать аутентификацию и авторизацию
- Работать с сессиями и cookies
- Использовать JWT токены
Аутентификация vs Авторизация
| Термин | Описание |
|---|---|
| Аутентификация | Кто ты? (логин/пароль) |
| Авторизация | Что тебе разрешено? (права) |
Sessions + Cookies
Установка
npm install express-sessionНастройка
const session = require('express-session');
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 1 день
}
}));Использование
// Логин
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !await user.comparePassword(password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Сохраняем в сессию
req.session.userId = user._id;
req.session.role = user.role;
res.json({ message: 'Logged in' });
});
// Проверка авторизации
function isAuthenticated(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
next();
}
// Защищённый маршрут
app.get('/profile', isAuthenticated, async (req, res) => {
const user = await User.findById(req.session.userId);
res.json(user);
});
// Логаут
app.post('/logout', (req, res) => {
req.session.destroy();
res.json({ message: 'Logged out' });
});Хранение сессий в Redis
npm install connect-redis redisconst RedisStore = require('connect-redis').default;
const redis = require('redis');
const redisClient = redis.createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}));JWT (JSON Web Token)
Установка
npm install jsonwebtokenГенерация токена
const jwt = require('jsonwebtoken');
function generateToken(user) {
return jwt.sign(
{ id: user._id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
}
// Логин
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+password');
if (!user || !await user.comparePassword(password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = generateToken(user);
res.json({ token, user: { id: user._id, email: user.email } });
});Проверка токена
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Token required' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
res.status(401).json({ error: 'Invalid token' });
}
}
// Использование
app.get('/profile', authenticate, async (req, res) => {
const user = await User.findById(req.user.id);
res.json(user);
});Refresh Token
function generateTokens(user) {
const accessToken = jwt.sign(
{ id: user._id },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ id: user._id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// Обновление токена
app.post('/refresh', async (req, res) => {
const { refreshToken } = req.body;
try {
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);
res.json(tokens);
} catch (error) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});Сравнение
| Критерий | Sessions | JWT |
|---|---|---|
| Хранение | Сервер | Клиент |
| Масштабирование | Сложнее | Проще |
| Отзыв | Легко | Сложно |
| Размер | Маленький | Больше |
Практика
Задание: JWT аутентификация
Задача: Реализуй регистрацию и логин с JWT.
Решение:
// Регистрация
app.post('/register', async (req, res) => {
const { email, password, name } = req.body;
const exists = await User.findOne({ email });
if (exists) {
return res.status(400).json({ error: 'Email exists' });
}
const user = await User.create({ email, password, name });
const token = generateToken(user);
res.status(201).json({ token, user: { id: user._id, email } });
});
// Логин
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+password');
if (!user || !await user.comparePassword(password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = generateToken(user);
res.json({ token });
});Проверь себя
- Чем JWT отличается от сессий?
- Зачем нужен refresh token?
- Где хранить JWT на клиенте?