Admin-Rollen
Teil des Abschnitts Authentifizierung und Sicherheit der Node.js-Journey von Coddy. Lektion 19 von 24.
GET /admin prüft nur, ob jemand angemeldet ist, daher wird es allen Mitgliedern angezeigt. Eine auf dem Benutzer gespeicherte Rolle, etwa 'member' oder 'admin', legt fest, was sie tun dürfen. Eine Middleware-Factory, also eine Funktion, die eine Middleware zurückgibt, prüft für jede Route eine Rolle:
function requirePlan(plan) {
return (req, res, next) => {
if (req.user.plan !== plan) {
return res.status(402).json({ error: `Needs the ${plan} plan` });
}
next();
};
}
app.get('/exports', requireUser, requirePlan('pro'), sendExports);Eine Rolle wird immer vom Server festgelegt. Bei der Registrierung werden aus req.body nur die erwarteten Felder übernommen, daher erhält ein Besucher, der "role": "admin" sendet, dadurch keine zusätzlichen Berechtigungen.
Aufgabe
EinfachGET /admin zeigt, wie viele Konten existieren, und derzeit kann jedes angemeldete Mitglied das sehen. Nur Administratoren sollten das können. Jeder Benutzer erhält eine vom Server festgelegte Rolle, und die Route überprüft sie.
- Speichere in
server.jsbeiPOST /signupjeden neuen Benutzer mit"role": "member". Übernimm die Rolle niemals aus dem Anforderungstext: Ein Besucher, der"role": "admin"sendet, wird trotzdem Mitglied. - Schreibe eine Funktion
requireRole(role), die eine Middleware zurückgibt. Diese Middleware überprüftreq.user.role:- Wenn der Wert nicht
roleentspricht, antworte mit Status 403 und{"error": "Not allowed"}. 403 Forbidden bedeutet, dass der Server weiß, wer du bist, du dies aber nicht tun darfst. Bei 401 weiß der Server nicht, wer du bist. - Wenn der Wert
roleentspricht, rufenext()auf.
- Wenn der Wert nicht
- Füge
requireRole('admin')beiGET /adminnachrequireUserein. Die Funktion liestreq.user, das nur vonrequireUsergesetzt wird.
Der Server erstellt beim Start bereits ein Administratorkonto. Sein Benutzername lautet admin, und sein Passwort ist admin-pass-for-tests, das aus der Umgebungsvariable ADMIN_PASSWORD stammt.
Probiere es aus: Drücke auf „Spielen“. Sende GET /admin: Status 401 und {"error": "Log in first"}. Nun versucht eine Benutzerin namens mallory, sich selbst zur Administratorin zu machen. Sende POST /signup mit {"username": "mallory", "password": "correct horse", "role": "admin"} und dann POST /login mit {"username": "mallory", "password": "correct horse"}. Sende GET /admin: Status 403 und {"error": "Not allowed"}, denn das zusätzliche Feld hat sie nicht zur Administratorin gemacht. Sende POST /login mit {"username": "admin", "password": "admin-pass-for-tests"} und dann GET /admin: {"users": 2}.
Probier es selbst
import express from 'express';
import { ObjectId } from 'mongodb';
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());
// Das einzige Admin-Konto. Sein Passwort stammt aus den Umgebungsvariablen.
await users.insertOne({ username: 'admin', role: 'admin', passwordHash: await hashPassword(process.env.ADMIN_PASSWORD) });
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,
sameSite: 'lax',
secure: true,
maxAge: 24 * 60 * 60 * 1000,
});
res.json({ username: user.username });
});
// Ermittelt den Benutzer anhand des Session-Cookies oder antwortet mit 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, role: user.role };
next();
}
app.get('/me', requireUser, (req, res) => {
res.json({ username: req.user.username });
});
app.post('/logout', async (req, res) => {
const { sid } = parseCookies(req.get('cookie'));
if (sid) {
await sessions.deleteOne({ _id: sid });
}
res.clearCookie('sid');
res.status(204).end();
});
// Deine Notizen als Webseite.
app.get('/', requireUser, async (req, res) => {
const list = await notes.find({ owner: req.user.id }).toArray();
const items = list.map((note) => `<li>${note.text}</li>`).join('');
res.send(`<!doctype html><title>Vault</title><h1>${req.user.username}'s notes</h1><ul>${items}</ul>`);
});
// Notizen: Jeder Benutzer liest und schreibt nur seine eigenen.
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.get('/notes/:id', requireUser, async (req, res) => {
if (!ObjectId.isValid(req.params.id)) {
return res.status(404).json({ error: 'Note not found' });
}
const note = await notes.findOne({ _id: new ObjectId(req.params.id), owner: req.user.id });
if (!note) {
return res.status(404).json({ error: 'Note not found' });
}
res.json(note);
});
app.delete('/notes/:id', requireUser, async (req, res) => {
if (!ObjectId.isValid(req.params.id)) {
return res.status(404).json({ error: 'Note not found' });
}
const { deletedCount } = await notes.deleteOne({ _id: new ObjectId(req.params.id), owner: req.user.id });
if (!deletedCount) {
return res.status(404).json({ error: 'Note not found' });
}
res.status(204).end();
});
// Für Admins: Wie viele Konten es gibt.
app.get('/admin', requireUser, async (req, res) => {
res.json({ users: await users.countDocuments() });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
Alle Lektionen in Authentifizierung und Sicherheit
1Passwörter
Bytes und Hex-TextEinen Passwortschlüssel ableitenEin neues SaltPasswörter niemals speichernSchlüssel-Bytes vergleichenEin Passwort überprüfenRegeln für die Registrierung4Eigentümer und Sicherheit
Nur deine NotizenAdmin-RollenÜbe selbstständig: Express-Playground