Menu
CoddyTech

Sesje logowania

Część sekcji Uwierzytelnianie i bezpieczeństwo ścieżki Node.js w Coddy. Lekcja 10 z 24.

Logowanie zwraca odpowiedź 200, a potem kolejne żądanie nie pamięta już, kto się zalogował. Sesja to zapamiętuje. Podczas logowania serwer tworzy identyfikator, którego nikt nie odgadnie, zapisuje, do którego użytkownika należy, i wysyła go jako ciasteczko. Każde kolejne żądanie przesyła ten identyfikator z powrotem, a serwer go wyszukuje.

Identyfikator musi być losowy. Ciasteczko zawierające nazwę użytkownika pozwoliłoby każdemu wpisać Cookie: sid=ada i podszyć się pod Adę. randomBytes(24) daje 24 losowe bajty, czyli 192 bity, których nikt nie odgadnie. toString('base64url') zapisuje je jako 32 litery, cyfry, - i _ — znaki bezpieczne w ciasteczku. Ustaw ciasteczko jako httpOnly, aby skrypty na stronie nie mogły go odczytać.

const cartId = randomBytes(24).toString('base64url');
await carts.insertOne({ _id: cartId, items: [] });
res.cookie('cart', cartId, { httpOnly: true });

// przy kolejnym żądaniu
const cart = await carts.findOne({ _id: parseCookies(req.get('cookie')).cart });
challenge icon

Wyzwanie

Łatwy

Ten krok otwiera przygotowany przez kurs projekt prywatnego API notatek, a nie pliki projektu z lekcji Zapamiętywanie za pomocą ciasteczek (w tym server.js). Twoja wcześniejsza praca pozostaje w swojej lekcji. Przed edycją server.js przeczytaj dostarczone pliki. Ta kopia zawiera trasy rejestracji i logowania do prywatnych notatek.

Logowanie działa, ale następne żądanie nie pamięta już, kto się zalogował. Spraw, by serwer zapamiętywał użytkownika za pomocą sesji: losowego identyfikatora zapisanego na serwerze i wysłanego do przeglądarki jako ciasteczko.

  1. W server.js zaimportuj randomBytes z node:crypto. Zaimportuj sessions z ./db.js obok notes i users. Zablokowany plik db.js ma już kolekcję sessions.
  2. W POST /login, po sprawdzeniu hasła, utwórz identyfikator sesji: 24 losowe bajty zapisane jako tekst base64url. To 32 znaki: litery, cyfry, - i _, na przykład oqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN. Przy każdym logowaniu utwórz nowy identyfikator.
  3. Zapisz sesję w sessions jako dokument z dwoma polami:
    • _id: identyfikator sesji.
    • userId: wartość _id zalogowanego użytkownika.
  4. Wyślij identyfikator do przeglądarki jako ciasteczko o nazwie sid, z opcją httpOnly, aby skrypty na stronie nie mogły go odczytać. Odpowiedzią logowania nadal jest {"username": "ada"}.
  5. Dodaj GET /me. Odczytuje ciasteczko sid, wyszukuje sesję z takim _id, a następnie wyszukuje użytkownika tej sesji. Odpowiedzią jest {"username": "ada"}. Jeśli nie ma ciasteczka sid albo nie ma sesji o tym identyfikatorze, serwer odpowiada statusem 401 i zwraca {"error": "Log in first"}. Osoba, która ręcznie wpisze ciasteczko, na przykład sid=ada, musi otrzymać odpowiedź 401.

Wypróbuj: Naciśnij „Uruchom”. Wyślij POST /signup z treścią {"username": "ada", "password": "correct horse"}. Wyślij GET /me: status 401 i {"error": "Log in first"}. Wyślij POST /login z tą samą treścią co przy rejestracji: status 200. Karta Headers odpowiedzi pokazuje Set-Cookie: sid=...; Path=/; HttpOnly, gdzie kropki zastępuje Twój losowy identyfikator. Ponownie wyślij GET /me. Przeglądarka odsyła ciasteczko, więc otrzymujesz {"username": "ada"}. Otwórz Data: sessions zawiera jeden dokument, w którym Twój identyfikator jest wartością _id, a wartość _id użytkowniczki ada jest wartością userId.

Spróbuj swoich sił

import express from 'express';
import { parseCookies } from './cookies.js';
import { notes, 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' });
    }
    res.json({ username: user.username });
});

// Zlicza, ile razy ta przeglądarka wysłała żądanie 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 });
});

// Notatki. Na razie każdy może je odczytywać i zapisywać.
app.get('/notes', async (req, res) => {
    res.json(await notes.find().toArray());
});

app.post('/notes', async (req, res) => {
    const note = { 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');
});

Wszystkie lekcje w sekcji Uwierzytelnianie i bezpieczeństwo

Poćwicz samodzielnie: Edytor online Express