Role administratora
Część sekcji Uwierzytelnianie i bezpieczeństwo ścieżki Node.js w Coddy. Lekcja 19 z 24.
Żądanie GET /admin sprawdza tylko, czy ktoś jest zalogowany, więc widzi je każdy użytkownik. Rola przypisana użytkownikowi, taka jak 'member' lub 'admin', określa, co może on robić. Fabryka middleware, czyli funkcja zwracająca middleware, sprawdza jedną rolę dla każdej trasy:
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);Rolę zawsze ustawia serwer. Podczas rejestracji kopiowane są tylko oczekiwane pola z req.body, więc osoba odwiedzająca, która wyśle "role": "admin", nic na tym nie zyska.
Wyzwanie
ŁatwyGET /admin pokazuje, ile istnieje kont, a obecnie każdy zalogowany członek może to zobaczyć. Tylko administratorzy powinni mieć do tego dostęp. Każdy użytkownik otrzymuje rolę ustawianą przez serwer, a trasa ją sprawdza.
- W pliku
server.js, wPOST /signup, zapisuj każdego nowego użytkownika z rolą"role": "member". Nigdy nie pobieraj roli z treści żądania: odwiedzający, który wyśle"role": "admin", i tak zostanie członkiem. - Napisz funkcję
requireRole(role), która zwraca oprogramowanie pośredniczące. To oprogramowanie sprawdzareq.user.role:- Jeśli wartość nie jest równa
role, zwróć odpowiedź ze statusem 403 i{"error": "Not allowed"}. 403 Forbidden oznacza, że serwer wie, kim jesteś, ale nie możesz tego zrobić. 401 oznacza, że serwer nie wie, kim jesteś. - Jeśli wartość jest równa
role, wywołajnext().
- Jeśli wartość nie jest równa
- Dodaj
requireRole('admin')doGET /admin, zarequireUser. Ta funkcja odczytujereq.user, które ustawia wyłącznierequireUser.
Serwer już podczas uruchamiania tworzy jedno konto administratora. Jego nazwa użytkownika to admin, a hasło to admin-pass-for-tests, które pochodzi ze zmiennej środowiskowej ADMIN_PASSWORD.
Wypróbuj: Naciśnij Uruchom. Wyślij GET /admin: status 401 i {"error": "Log in first"}. Teraz użytkowniczka o nazwie mallory próbuje nadać sobie uprawnienia administratora. Wyślij POST /signup z {"username": "mallory", "password": "correct horse", "role": "admin"}, a następnie POST /login z {"username": "mallory", "password": "correct horse"}. Wyślij GET /admin: status 403 i {"error": "Not allowed"}, ponieważ dodatkowe pole nie nadało jej uprawnień administratora. Wyślij POST /login z {"username": "admin", "password": "admin-pass-for-tests"}, a następnie GET /admin: {"users": 2}.
Spróbuj swoich sił
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());
// Jedyne konto administratora. Jego hasło pochodzi ze zmiennej środowiskowej.
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 });
});
// Znajduje użytkownika na podstawie ciasteczka sesyjnego albo zwraca 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();
});
// Twoje notatki wyświetlane jako strona.
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>`);
});
// Notatki: każdy użytkownik odczytuje i zapisuje tylko własne.
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();
});
// Dla administratorów: liczba kont.
app.get('/admin', requireUser, async (req, res) => {
res.json({ users: await users.countDocuments() });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
Wszystkie lekcje w sekcji Uwierzytelnianie i bezpieczeństwo
1Hasła
Bajty i zapis szesnastkowyWyprowadzanie klucza z hasłaNowa sólNigdy nie przechowuj hasełPorównywanie bajtów kluczaSprawdzanie hasłaZasady rejestracji4Właściciele i bezpieczeństwo
Tylko twoje notatkiRole administratoraPoćwicz samodzielnie: Edytor online Express