Menu
CoddyTech

Routen hinter einer Anmeldung

Teil des Abschnitts Authentifizierung und Sicherheit der Node.js-Journey von Coddy. Lektion 11 von 24.

Jede geschützte Route benötigt den Sitzungsabruf von GET /me. Wird er in jede Route kopiert, wird er irgendwann vergessen, und diese Route ist für alle zugänglich. Schreibe ihn stattdessen einmal als Middleware.

Middleware wird vor der Route ausgeführt. Wenn sie etwas an req anhängt, etwa req.user, liest die Route es von dort aus. Die _id eines Benutzers ist ein ObjectId; user._id.toHexString() gibt sie als Text zurück. Die Middleware antwortet entweder und beendet die Verarbeitung oder hängt das Gefundene an req an und ruft next() auf. Übergib sie an jede Route, die sie benötigt, vor dem Handler:

function requireAdult(req, res, next) {
    if (Number(req.query.age) < 18) {
        return res.status(403).json({ error: 'Adults only' });
    }
    req.adult = true;
    next();
}

app.get('/wines', requireAdult, (req, res) => res.json(wines));

Das return ist wichtig: Ohne es würde auch die Route ausgeführt.

challenge icon

Aufgabe

Einfach

Notizen sind weiterhin für alle zugänglich. Mach sie privat: Nur angemeldete Benutzer können Notizen lesen oder hinzufügen, und jeder Benutzer sieht nur seine eigenen.

  1. Schreibe in server.js eine Middleware-Funktion requireUser(req, res, next). Verschiebe die Sitzungssuche aus GET /me dorthin: Lies das Cookie sid, suche die Sitzung und anschließend den zugehörigen Benutzer.
    • Wenn es keinen solchen Benutzer gibt, antworte mit Status 401 und {"error": "Log in first"} und brich dort ab.
    • Andernfalls weise den Benutzer der Anfrage als req.user zu, einem Objekt mit zwei Feldern, und rufe dann next() auf. Die beiden Felder sind id, die _id des Benutzers als Text, und username. Die Methode toHexString() einer ObjectId liefert den Text, zum Beispiel "66f1c2a9e4b0a1b2c3d4e5f6".
  2. Übergib requireUser an GET /me, GET /notes und POST /notes, jeweils vor der eigenen Funktion der Route. GET /me antwortet dann mit {"username": "ada"}, ausgelesen aus req.user.
  3. Speichere in POST /notes jede Notiz mit einem Feld owner, das req.user.id enthält. Die Antwort ist die neue Notiz: {"owner": "66f1c2a9e4b0a1b2c3d4e5f6", "text": "Door code 1234", "_id": "66f1c2b0e4b0a1b2c3d4e5f7"}
  4. Gib in GET /notes nur die Notizen zurück, deren owner der angemeldete Benutzer ist.

Probiere es aus: Drücke „Ausführen“. Sende GET /notes: Status 401 und {"error": "Log in first"}. Sende POST /signup und anschließend POST /login, beide mit {"username": "ada", "password": "correct horse"}. Sende POST /notes mit {"text": "Door code 1234"}: Status 201 und die Notiz mit ihrem owner. Sende GET /notes: eine Liste mit dieser einen Notiz. Registriere dich nun und melde dich mit {"username": "bob", "password": "correct horse"} an. GET /notes gibt [] zurück, weil bob noch nichts geschrieben hat.

Probier es selbst

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

app.get('/me', async (req, res) => {
    const { sid } = parseCookies(req.get('cookie'));
    const session = sid && (await sessions.findOne({ _id: sid }));
    if (!session) {
        return res.status(401).json({ error: 'Log in first' });
    }
    const user = await users.findOne({ _id: session.userId });
    res.json({ username: user.username });
});

// Zählt, wie oft dieser Browser GET /visit aufgerufen hat.
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 });
});

// Notizen. Vorerst kann jeder sie lesen und schreiben.
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');
});
quiz iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Authentifizierung und Sicherheit

Übe selbstständig: Express-Playground