Dossiers d’images par utilisateur avec ASP.NET Core

Le dossier uploads de l’exemple ASP.NET Core sur le disque, avec des dossiers partagés et un dossier users contenant alice et bob, chacun avec ses propres photos

Par défaut, la galerie montre les mêmes dossiers à tout le monde, ce qui convient à un site d’entreprise. Dans un forum ou une application multiclient, chaque personne ne doit voir que ses propres images. Dans les exemples ASP.NET Core, cela tient à une seule méthode, RootFor(HttpContext), qui décide où se trouvent les fichiers d’une requête. Cette page présente la version par utilisateur, la connexion qu’elle nécessite et ce qui reste public.

Une seule méthode décide

Chaque route (liste, création de dossier, envoi) demande à RootFor un dossier sur le disque et l’URL sous laquelle il est servi. Rien d’autre dans le code ne connaît les utilisateurs.

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 dans appsettings.json vaut false par défaut. Avec true, chaque utilisateur connecté reçoit uploads/users/<id>/ comme racine de sa galerie. Son bouton New folder et ses envois fonctionnent à l’intérieur, et .. ne permet pas d’en sortir, car la vérification du dossier s’effectue par rapport à cette racine.
  • L’identifiant est ClaimTypes.NameIdentifier. Avec ASP.NET Core Identity, cette revendication contient l’identifiant de l’utilisateur, un GUID, qui ne change jamais. Un nom d’utilisateur ou une adresse e-mail peut changer, et sous Windows Alice et alice seraient un seul et même dossier.
  • Pas d’utilisateur, pas de galerie. Une requête anonyme reçoit 401 {"error":"Sign in to use the image gallery."}, et la boîte de dialogue affiche son propre message « The image gallery is unavailable right now (http-401). » plutôt que le texte du serveur.
  • L’identifiant est vérifié comme un nom de dossier. Un identifiant comme ../../x reçoit un 403 au lieu de devenir un chemin.

La connexion

L’exemple utilise l’authentification par cookie, le schéma avec lequel ASP.NET Core Identity connecte aussi les utilisateurs. La connexion propre à votre application remplace la connexion de test ci-dessous.

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

Pour essayer les dossiers par utilisateur sans écrire de page de connexion, l’exemple déclare une connexion réservée aux tests, et uniquement dans l’environnement Development. Elle connecte n’importe qui sous n’importe quelle identité : elle ne doit donc jamais arriver sur un vrai serveur.

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("/");
    });
}

Avec un simple dotnet run, qui s’exécute en Production, /demo/sign-in a répondu 404.

Ce qui reste public

Des galeries séparées ne rendent pas les fichiers privés. /uploads/users/alice/harbor-fd5ff24b.jpg est servi par le même mappage de fichiers statiques que tous les autres fichiers envoyés, et il a répondu 200 à une requête sans cookie. C’est généralement ce qu’il faut pour les images d’un contenu publié : les lecteurs d’un article doivent pouvoir charger ses images. La partie aléatoire du nom de fichier empêche quiconque de deviner l’URL, mais toute personne à qui elle est communiquée peut l’ouvrir. Si les images d’un utilisateur doivent rester privées, servez-les depuis un point de terminaison qui vérifie l’utilisateur plutôt que depuis UseStaticFiles. L’exemple n’en contient pas.

Activer les dossiers par utilisateur ne décide pas non plus qui a le droit d’envoyer des fichiers. Ajoutez [Authorize] au contrôleur, ou .RequireAuthorization() au groupe Minimal API, dès que l’application dispose d’une connexion.

Tester

Démarrez le projet d’exemple de ce guide en Development avec les dossiers par utilisateur activés, puis travaillez en tant que deux utilisateurs avec deux fichiers de cookies :

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}]}

La galerie de Bob ne liste que son image. Dans un navigateur, ouvrez http://localhost:5302/demo/sign-in?user=alice et cliquez sur Insert Gallery : seuls les images et les dossiers d’Alice s’affichent. L’image en haut de cette page montre ensuite le dossier uploads, avec users/alice et users/bob à côté des dossiers partagés qu’utilise l’exemple lorsque PerUserFolders est désactivé.

Le même guide pour d’autres plateformes

JavaScript · PHP · MVC 5 · Web Forms

À lire ensuite