JS Tower
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 redis
const 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' });
  }
});

Сравнение

КритерийSessionsJWT
ХранениеСерверКлиент
МасштабированиеСложнееПроще
ОтзывЛегкоСложно
РазмерМаленькийБольше

Практика

Задание: 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 });
});

Проверь себя

  1. Чем JWT отличается от сессий?
  2. Зачем нужен refresh token?
  3. Где хранить JWT на клиенте?