Menu
CoddyTech

Выход из системы

Часть раздела Аутентификация и безопасность путешествия по Node.js на Coddy. Урок 12 из 24.

Очистка cookie лишь просит браузер забыть идентификатор. У любого, кто скопировал идентификатор с общего компьютера или из украденной cookie, всё ещё есть действующий ключ. При выходе из системы сессию нужно удалить на сервере.

res.clearCookie(name) сообщает браузеру, что нужно удалить cookie. Статус 204 No Content означает, что всё прошло успешно и отправлять в ответ нечего: res.status(204).end().

Вот связанный маршрут, который одновременно выводит пользователя из системы на всех устройствах:

app.post('/logout-everywhere', requireUser, async (req, res) => {
    await sessions.deleteMany({ userId: new ObjectId(req.user.id) });
    res.clearCookie('sid');
    res.status(204).end();
});
challenge icon

Задание

Легко

Добавь возможность выйти из системы. Удалить cookie недостаточно, потому что любой, кто скопировал идентификатор сессии, сможет продолжать им пользоваться. Поэтому сервер тоже забывает сессию.

  1. В server.js добавь POST /logout. Тело запроса не требуется.
  2. Прочитай cookie sid. Если она есть, удали из sessions сессию с этим _id.
  3. Укажи браузеру забыть cookie sid.
  4. Ответь со статусом 204 и без тела. 204 «Нет содержимого» означает, что запрос выполнен успешно и отправлять в ответ нечего. Отвечай со статусом 204, даже если в запросе не было cookie sid.

Попробуй: Нажми «Играть». Отправь POST /signup, затем POST /login, в обоих случаях с {"username": "ada", "password": "correct horse"}. GET /me возвращает {"username": "ada"}. Отправь POST /logout: статус 204 и пустое тело. Снова отправь GET /me: статус 401 и {"error": "Log in first"}. Открой «Данные»: sessions пуст. Отправь POST /logout ещё раз: ответ по-прежнему 204.

Попробуйте сами

import express from 'express';
import { randomBytes } from 'node:crypto';
import { parseCookies } from './cookies.js';
import { notes, sessions, users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';

const app = express();
app.use(express.json());

app.post('/signup', async (req, res) => {
    const username = String(req.body.username ?? '').trim().toLowerCase();
    const password = String(req.body.password ?? '');
    if (!username || password.length < 8) {
        return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
    }
    if (await users.findOne({ username })) {
        return res.status(409).json({ error: 'That username is taken' });
    }
    await users.insertOne({ username, passwordHash: await hashPassword(password) });
    res.status(201).json({ username });
});

app.post('/login', async (req, res) => {
    const username = String(req.body.username ?? '').trim().toLowerCase();
    const user = await users.findOne({ username });
    if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
        return res.status(401).json({ error: 'Wrong username or password' });
    }
    const sid = randomBytes(24).toString('base64url');
    await sessions.insertOne({ _id: sid, userId: user._id, createdAt: new Date() });
    res.cookie('sid', sid, { httpOnly: true });
    res.json({ username: user.username });
});

// Находит пользователя по cookie сессии или возвращает 401.
async function requireUser(req, res, next) {
    const { sid } = parseCookies(req.get('cookie'));
    const session = sid && (await sessions.findOne({ _id: sid }));
    const user = session && (await users.findOne({ _id: session.userId }));
    if (!user) {
        return res.status(401).json({ error: 'Log in first' });
    }
    req.user = { id: user._id.toHexString(), username: user.username };
    next();
}

app.get('/me', requireUser, (req, res) => {
    res.json({ username: req.user.username });
});

// Подсчитывает, сколько раз этот браузер отправлял запрос GET /visit.
app.get('/visit', (req, res) => {
    const cookies = parseCookies(req.get('cookie'));
    const visits = Number(cookies.visits ?? 0) + 1;
    res.cookie('visits', String(visits));
    res.json({ visits });
});

// Заметки: каждый пользователь читает и записывает только свои.
app.get('/notes', requireUser, async (req, res) => {
    res.json(await notes.find({ owner: req.user.id }).toArray());
});

app.post('/notes', requireUser, async (req, res) => {
    const note = { owner: req.user.id, text: String(req.body.text ?? '') };
    await notes.insertOne(note);
    res.status(201).json(note);
});

app.listen(3000, () => {
    console.log('Vault is running on port 3000');
});

Все уроки раздела Аутентификация и безопасность

Потренируйтесь самостоятельно: Песочница Express