Menu
CoddyTech

Sessioni di accesso

Fa parte della sezione Autenticazione e sicurezza del percorso Node.js di Coddy. Lezione 10 di 24.

Il login risponde con 200, poi la richiesta successiva ha dimenticato chi ha effettuato l’accesso. Una sessione lo ricorda. Al momento dell’accesso, il server crea un ID impossibile da indovinare, memorizza a quale utente appartiene e lo invia come cookie. Ogni richiesta successiva rimanda l’ID e il server lo cerca.

L’ID deve essere casuale. Un cookie che contiene il nome utente permetterebbe a chiunque di digitare Cookie: sid=ada e diventare Ada. randomBytes(24) fornisce 24 byte casuali, 192 bit, impossibili da indovinare. toString('base64url') li scrive come 32 lettere, cifre, - e _, caratteri sicuri da usare in un cookie. Imposta il cookie httpOnly in modo che gli script nella pagina non possano leggerlo.

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

// in una richiesta successiva
const cart = await carts.findOne({ _id: parseCookies(req.get('cookie')).cart });
challenge icon

Sfida

Facile

Questo passaggio apre il progetto API per le note private predisposto per il corso, anziché i file del tuo progetto di Ricordare con i cookie (incluso server.js). Il lavoro precedente rimane nella sua lezione. Leggi i file forniti prima di modificare server.js. Questa copia contiene le route di registrazione e accesso per le note private.

L'accesso funziona, ma la richiesta successiva ha già dimenticato chi ha effettuato l'accesso. Fai in modo che il server ricordi l'utente tramite una sessione: un ID casuale salvato sul server e inviato al browser come cookie.

  1. In server.js, importa randomBytes da node:crypto. Importa sessions da ./db.js insieme a notes e users. Il file bloccato db.js contiene già una raccolta sessions.
  2. In POST /login, dopo aver verificato la password, crea un ID di sessione: 24 byte casuali convertiti in testo base64url. Sono 32 caratteri tra lettere, cifre, - e _, come oqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN. Crea un nuovo ID a ogni accesso.
  3. Salva la sessione in sessions come documento con due campi:
    • _id: l'ID di sessione.
    • userId: il _id dell'utente che ha effettuato l'accesso.
  4. Invia l'ID al browser come cookie denominato sid, con l'opzione httpOnly, in modo che gli script nella pagina non possano leggerlo. L'accesso continua a rispondere con {"username": "ada"}.
  5. Aggiungi GET /me. Legge il cookie sid, cerca la sessione con quell'_id, poi trova l'utente associato a quella sessione. Risponde con {"username": "ada"}. Se non c'è il cookie sid, o nessuna sessione ha quell'ID, risponde con lo stato 401 e {"error": "Log in first"}. Chi inserisce manualmente un cookie, come sid=ada, deve ricevere lo stato 401.

Prova: premi «Gioca». Invia POST /signup con {"username": "ada", "password": "correct horse"}. Invia GET /me: stato 401 e {"error": "Log in first"}. Invia POST /login con lo stesso corpo della registrazione: stato 200. La scheda Headers della risposta mostra Set-Cookie: sid=...; Path=/; HttpOnly, con il tuo ID casuale al posto dei puntini. Invia di nuovo GET /me. Il browser rimanda il cookie, quindi ricevi {"username": "ada"}. Apri Data: sessions contiene un documento, con il tuo ID come _id e l'_id di ada come userId.

Provalo tu

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 });
});

// Conta quante volte questo browser ha chiamato 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 });
});

// Note. Per ora chiunque può leggerle e scriverle.
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');
});

Tutte le lezioni di Autenticazione e sicurezza

Esercitati da solo: Playground Express