Bildordner pro Benutzer in Node.js

Der Ordner uploads des Express-Beispiels mit den gemeinsamen Ordnern blog, products und team und einem Ordner users mit alice und bob, jeweils mit eigenen Bildern

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>/:

JavaScript · server.mjs
// 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:

JavaScript · server.mjs
// 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:

JavaScript · gallery.mjs
// 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:

Shell
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

Weiterlesen