Bildordner pro Benutzer in Node.js

Wenn jede Person ihre eigenen Inhalte schreibt, sollte die Galerie jeder Person ihre eigenen Bilder zeigen, nicht die aller anderen. Im Express-Beispiel ist das die Änderung einer einzigen Funktion, rootFor(req), die für jeden Request den Stammordner für die Speicherung wählt. Diese Seite zeigt diese Funktion, in welcher Reihenfolge zu multer sie laufen muss und was Ordner pro Benutzer nicht schützen.
Eine Funktion wählt den Stammordner
Alles, was die Galerie tut (auflisten, einen Ordner anlegen, hochladen), geschieht innerhalb des Ordners, den rootFor(req) zurückgibt. Standardmäßig ist das der gemeinsame Ordner uploads/. Mit PER_USER=1 ist es uploads/users/<id>/:
// The storage root for this request. Everything the gallery lists, creates or uploads
// stays inside it. Keep it inside UPLOADS_DIR, so the /uploads URLs still work.
// PER_USER=1 gives every user uploads/users/<id>/.
const PER_USER = process.env.PER_USER === "1";
function rootFor(req) {
if (!PER_USER) return UPLOADS_DIR;
const user = demoUser(req); // with real logins: req.user.id from your session middleware
if (!user) throw new GalleryError(401, "Sign in to use the gallery.");
return path.join(UPLOADS_DIR, "users", user);
}
// Stand-in for a login, for trying PER_USER locally: /demo-login?user=alice sets a cookie.
// Anyone can set any cookie, so this is NOT authentication.
function demoUser(req) {
const match = /(?:^|;\s*)demo_user=([a-z0-9]{1,32})(?:;|$)/.exec(req.headers.cookie ?? "");
return match ? match[1] : null;
}Sonst ändert sich nichts. Alices Ordner drafts ist auf der Festplatte uploads/users/alice/drafts, der Dialog nennt ihn drafts, und die Antwort auf den Upload lautet /uploads/users/alice/drafts/forest-5e6f80c8.jpg. Die URL funktioniert, weil der Stammordner pro Benutzer innerhalb von UPLOADS_DIR liegt und das Beispiel jede URL aus dem Pfad unterhalb von UPLOADS_DIR bildet. Wenn Sie einen Ordner außerhalb davon zurückgeben, sind die URLs falsch.
Vor multer, nicht danach
Der Stammordner wird in einer Middleware auf /rte gewählt, vor multer:
// Every /rte request gets its storage root first, before multer reads the body.
app.use("/rte", (req, res, next) => {
req.storageRoot = rootFor(req);
next();
});
// GET /rte/gallery?folder=products -> listing
app.get("/rte/gallery", async (req, res) => {
res.json(await listFolder(req.storageRoot, req.query.folder ?? ""));
});
// POST /rte/gallery action=create-folder, folder, name -> listing of the parent folder
app.post("/rte/gallery", parseForm.none(), async (req, res) => {
const { action, folder, name } = req.body ?? {};
if (action !== "create-folder") throw new GalleryError(400, "Unknown action.");
res.json(await createFolder(req.storageRoot, folder ?? "", name));
});
// POST /rte/upload file, folder -> { url }
app.post("/rte/upload", parseForm.single("file"), async (req, res) => {
if (!req.file) throw new GalleryError(400, "No file was uploaded.");
const url = await saveImage(req.storageRoot, req.body.folder ?? "", req.file.originalname, req.file.buffer);
res.json({ url });
});Die Reihenfolge ist wichtig. Liefe rootFor() im Upload-Handler, würde multer zuerst bis zu 4,000,000 Bytes von jedem lesen, ob angemeldet oder nicht, und erst danach würde der Request abgelehnt. Als Middleware wirft die Funktion GalleryError(401), bevor der Body geparst wird, und Express übergibt den Fehler an die Fehler-Middleware. Setzen Sie Ihre eigene Login-Middleware (Sessions, passport, eine JWT-Prüfung) auf dieselbe Weise davor.
Woher die Benutzer-ID kommt
Die ID muss aus Ihrem Login stammen, niemals aus etwas, das der Browser sendet. demoUser() im Beispiel liest ein Cookie, das /demo-login?user=alice setzt, sodass Sie zwei Benutzer ohne Login-System ausprobieren können. Diese Route gibt es nur mit PER_USER=1, und sie akzeptiert nur a-z und 0-9. Jeder kann jedes beliebige Cookie setzen, es ist also nur ein Platzhalter, keine Authentifizierung. Ersetzen Sie den Aufruf durch req.user.id oder durch das, was Ihre Session-Middleware bereitstellt. Ist diese ID kein einfaches Token (etwa eine E-Mail-Adresse), bilden Sie sie zuerst auf eine numerische ID ab: Sie wird zu einem Ordnernamen.
Ohne Benutzer antwortet jeder Request an /rte mit 401 {"error":"Sign in to use the gallery."}. Der Dialog zeigt dann „The image gallery is unavailable right now (http-401).“
Gemeinsame Ordner und Ordner pro Benutzer auf einer Festplatte
Die Stammordner pro Benutzer liegen in uploads/users/. Wird derselbe Ordner auch im gemeinsamen Modus verwendet, würde die gemeinsame Galerie users auflisten, und jeder könnte in die Ordner aller anderen wechseln. Deshalb behandelt der gemeinsame Stammordner users als reserviert:
// Per-user roots live in uploads/users/<id>/. The shared gallery must not show or touch them.
function isUsersFolder(root, folder) {
return path.resolve(root) === UPLOADS_DIR && String(folder ?? "").split("/")[0].toLowerCase() === "users";
}Im gemeinsamen Modus lässt die Liste des Stammordners users weg, ?folder=users/alice erhält ein 404, ein Upload dorthin ein 404, und das Anlegen eines Ordners namens users ein 400. Der Vergleich ignoriert Groß- und Kleinschreibung, damit ist auch Users unter Windows abgedeckt. Das Bild oben zeigt beides auf einer Festplatte: die gemeinsamen Ordner blog, products und team sowie users/alice und users/bob.
Was das nicht schützt
Die Bilder sind weiterhin statische Dateien unter /uploads/. Wir haben Alices Bild ganz ohne Cookie geöffnet und ein 200 erhalten. Der zufällige Teil des Namens macht die URLs schwer zu erraten, aber nicht geheim: Sie stehen im HTML jedes Dokuments, das das Bild anzeigt. Wenn Bilder privat sein müssen, ersetzen Sie das Einbinden von express.static durch eine Route, die den Benutzer prüft, bevor sie die Datei sendet.
Testen
Starten Sie das Beispielprojekt zu dieser Anleitung im Modus mit Ordnern pro Benutzer mit PER_USER=1 npm start. In PowerShell führen Sie zuerst $env:PER_USER=1 aus, dann npm start. curls Option -b sendet das Platzhalter-Cookie:
curl "http://localhost:5101/rte/gallery?folder="
{"error":"Sign in to use the gallery."}
curl -b demo_user=bob -F "file=@photo.jpg" http://localhost:5101/rte/upload
{"url":"/uploads/users/bob/photo-cbf27791.jpg"}
curl -b demo_user=bob "http://localhost:5101/rte/gallery?folder=../alice"
{"error":"Invalid folder."}Öffnen Sie im Browser http://localhost:5101/demo-login?user=alice und klicken Sie auf Insert Gallery. Der Dialog zeigt nur Alices Ordner und Bilder. Öffnen Sie /demo-login?user=bob, und der Dialog zeigt nur die von Bob.
Dieselbe Anleitung für andere Plattformen
PHP · ASP.NET Core · MVC 5 · Web Forms