Bildordner pro Benutzer in ASP.NET Core

Der Ordner uploads des ASP.NET-Core-Beispiels auf der Festplatte mit gemeinsamen Ordnern und einem Ordner users mit alice und bob, jeweils mit eigenen Fotos

Standardmäßig zeigt die Galerie allen dieselben Ordner, was für eine Unternehmenswebsite passt. In einem Forum oder einer App für mehrere Kunden sollte jede Person nur ihre eigenen Bilder sehen. In den ASP.NET-Core-Beispielen ist das eine einzige Methode, RootFor(HttpContext), die entscheidet, wo die Dateien eines Requests liegen. Diese Seite zeigt die Variante pro Benutzer, die Anmeldung, die sie braucht, und was öffentlich bleibt.

Eine Methode entscheidet

Jede Route (auflisten, Ordner anlegen, hochladen) fragt RootFor nach einem Ordner auf der Festplatte und der URL, unter der er ausgeliefert wird. Sonst weiß nichts im Code etwas von Benutzern.

C# · RteFiles.cs
// Where this request's files live. This is the only method to change for per-user folders.
//   "PerUserFolders": false (default)  everyone shares <content root>/uploads, served at /uploads
//   "PerUserFolders": true             each signed-in user gets uploads/users/<user id>
public static RteRoot RootFor(HttpContext http)
{
    var uploads = UploadsFolder(http.RequestServices.GetRequiredService<IWebHostEnvironment>());
    if (!http.RequestServices.GetRequiredService<IConfiguration>().GetValue<bool>("PerUserFolders"))
        return new RteRoot(uploads, "/uploads");

    // Use the user's id (a GUID with ASP.NET Core Identity), not the user name, which can change.
    var id = http.User.FindFirstValue(ClaimTypes.NameIdentifier)
             ?? throw new RteError(401, "Sign in to use the image gallery.");
    // The id becomes a folder name, so it must pass the same rules as any other folder name.
    if (!IsValidName(id)) throw new RteError(403, "This user id cannot be used as a folder name.");
    return new RteRoot(Path.Combine(uploads, "users", id), "/uploads/users/" + Uri.EscapeDataString(id));
}
  • PerUserFolders in appsettings.json ist standardmäßig false. Steht der Wert auf true, erhält jeder angemeldete Benutzer uploads/users/<id>/ als Stammordner seiner Galerie. Seine Schaltfläche „New folder“ und seine Uploads arbeiten darin, und .. kann ihn nicht verlassen, weil die Ordnerprüfung gegen diesen Stammordner läuft.
  • Die ID ist ClaimTypes.NameIdentifier. Mit ASP.NET Core Identity enthält dieser Claim die ID des Benutzers, eine GUID, die sich nie ändert. Ein Benutzername oder eine E-Mail-Adresse kann sich ändern, und unter Windows wären Alice und alice derselbe Ordner.
  • Kein Benutzer, keine Galerie. Ein anonymer Request erhält 401 {"error":"Sign in to use the image gallery."}, und der Dialog zeigt seine eigene Meldung „The image gallery is unavailable right now (http-401).“ statt des Textes des Servers.
  • Die ID wird wie ein Ordnername geprüft. Eine ID wie ../../x erhält ein 403 statt eines Pfads.

Anmeldung

Das Beispiel verwendet Cookie-Authentifizierung, das Schema, mit dem auch ASP.NET Core Identity anmeldet. Das eigene Login Ihrer App ersetzt die Testanmeldung unten.

C# · Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
// Your app's sign-in goes here. "PerUserFolders": true in appsettings.json gives each user a folder.
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
var app = builder.Build();

app.UseStaticFiles();
app.UseRteStaticFiles();          // /uploads, /richtexteditor, /rte-files.js
app.UseAuthentication();
app.UseAuthorization();

app.MapDefaultControllerRoute();  // HomeController.Index at "/", plus RteController's [Route] attributes

Damit Sie Ordner pro Benutzer ausprobieren können, ohne ein Login zu bauen, bildet das Beispiel eine reine Testanmeldung ab, und zwar nur in der Umgebung Development. Sie meldet jeden als jeden an und darf deshalb niemals auf einen echten Server gelangen:

C# · Program.cs
// TEST ONLY, and mapped only in Development: /demo/sign-in?user=alice signs anyone in as anyone,
// so per-user folders can be tried without a real login. Never ship this.
if (app.Environment.IsDevelopment())
{
    app.MapGet("/demo/sign-in", async (HttpContext http, string user) =>
    {
        var identity = new ClaimsIdentity([new Claim(ClaimTypes.NameIdentifier, user)], CookieAuthenticationDefaults.AuthenticationScheme);
        await http.SignInAsync(new ClaimsPrincipal(identity));
        return Results.Redirect("/");
    });
}

Mit einfachem dotnet run, das als Production läuft, antwortete /demo/sign-in mit 404.

Was öffentlich bleibt

Getrennte Galerien machen die Dateien nicht privat. /uploads/users/alice/harbor-fd5ff24b.jpg wird über dieselbe Zuordnung für statische Dateien ausgeliefert wie jeder andere Upload und antwortete auf einen Request ohne Cookie mit 200. Für Bilder in veröffentlichten Inhalten ist das meist richtig: Leser eines Beitrags müssen seine Bilder laden können. Der zufällige Teil des Dateinamens sorgt dafür, dass niemand die URL erraten kann, aber jeder, mit dem sie geteilt wird, kann sie öffnen. Wenn die Bilder eines Benutzers privat bleiben müssen, liefern Sie sie über einen Endpoint aus, der den Benutzer prüft, statt über UseStaticFiles. Das Beispiel enthält keinen solchen Endpoint.

Das Einschalten von Ordnern pro Benutzer entscheidet auch nicht darüber, wer überhaupt hochladen darf. Ergänzen Sie [Authorize] am Controller oder .RequireAuthorization() an der Minimal-API-Gruppe, sobald die App eine Anmeldung hat.

Testen

Starten Sie das Beispielprojekt zu dieser Anleitung in Development mit eingeschalteten Ordnern pro Benutzer und arbeiten Sie dann mit zwei Cookie-Dateien als zwei Benutzer:

Shell
dotnet run -- --environment Development --PerUserFolders true

curl "http://localhost:5302/rte/gallery?folder="
{"error":"Sign in to use the image gallery."}

curl -s -o /dev/null -c alice.jar "http://localhost:5302/demo/sign-in?user=alice"
curl -b alice.jar -F "file=@harbor.jpg" http://localhost:5302/rte/upload
{"url":"/uploads/users/alice/harbor-fd5ff24b.jpg"}

curl -s -o /dev/null -c bob.jar "http://localhost:5302/demo/sign-in?user=bob"
curl -b bob.jar -F "file=@forest.jpg" http://localhost:5302/rte/upload
{"url":"/uploads/users/bob/forest-8b1a0f16.jpg"}

curl -b bob.jar "http://localhost:5302/rte/gallery?folder="
{"currentFolder":"","currentFolderDisplay":"/","parentFolder":null,"folders":[],"images":[{"url":"/uploads/users/bob/forest-8b1a0f16.jpg","name":"forest-8b1a0f16.jpg","folder":"","size":36337}]}

Bobs Galerie listet nur sein Bild auf. Öffnen Sie im Browser http://localhost:5302/demo/sign-in?user=alice und klicken Sie auf Insert Gallery: Der Dialog zeigt nur Alices Bilder und Ordner. Das Bild oben auf dieser Seite zeigt den Ordner uploads danach, mit users/alice und users/bob neben den gemeinsamen Ordnern, die das Beispiel verwendet, solange PerUserFolders ausgeschaltet ist.

Dieselbe Anleitung für andere Plattformen

JavaScript · PHP · MVC 5 · Web Forms

Weiterlesen