Dossiers d’images par utilisateur avec ASP.NET Web Forms

Avec un seul dossier uploads/ partagé, chaque utilisateur de l’éditeur voit les images de tous les autres dans la galerie. L’exemple Web Forms change cela avec une seule méthode, RteFiles.RootFor, et un seul réglage appSettings. Avec ce réglage activé, chaque utilisateur connecté reçoit uploads/users/<id>/ et les requêtes anonymes reçoivent un 401. Cette page présente la modification, la redirection de l’authentification Forms qui casse le 401, et ce que les dossiers par utilisateur ne protègent pas.
Le réglage
<appSettings>
<!-- true: each signed-in user gets uploads/users/<id>/ (see RteFiles.RootFor) -->
<add key="rte:PerUserFolders" value="false" />
</appSettings>Il est désactivé par défaut, et dans ce cas l’exemple se comporte exactement comme dans le guide sur la galerie. Il est lu avec WebConfigurationManager.AppSettings, comme toute autre valeur appSettings.
RootFor
// The storage root for this request, as an app-relative virtual path. Files are written
// to Server.MapPath(root) and served from the same URL, so it stays inside ~/uploads,
// which web.config locks down to static files only.
//
// Default: one shared folder. With <add key="rte:PerUserFolders" value="true" /> in
// web.config, every signed-in user gets ~/uploads/users/<id>/ and anonymous requests get
// 401. /uploads/... is still a public URL: per-user roots keep users out of each other's
// folders in the dialog, they do not make the files private.
public static string RootFor(HttpContext context)
{
if (WebConfigurationManager.AppSettings["rte:PerUserFolders"] != "true")
return "~/uploads";
IPrincipal user = context.User;
if (user == null || !user.Identity.IsAuthenticated)
throw new RteException(401, "Sign in to use the image gallery.");
return "~/uploads/users/" + FolderIdFor(user.Identity.Name);
}
// A folder name for a user: the same user always gets the same folder, and nothing the
// user typed reaches the disk path (a name like "DOMAIN\bob" or "a@b.com" is not a folder name).
// If your users have a numeric id in your database, return that instead.
static string FolderIdFor(string userName)
{
using (SHA256 sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(userName.ToLowerInvariant()));
return BitConverter.ToString(hash, 0, 8).Replace("-", "").ToLowerInvariant();
}
}RootFor renvoie un chemin virtuel relatif à l’application. Tout le reste s’appuie dessus. Server.MapPath le transforme en racine sur le disque, par rapport à laquelle chaque dossier est vérifié, et VirtualPathUtility.ToAbsolute le transforme en préfixe d’URL de chaque image. La liste, la création de dossier et l’envoi basculent donc ensemble. Le dossier est créé la première fois que l’utilisateur ouvre la galerie.
Le nom du dossier provient de HttpContext.User, mais jamais tel qu’il a été saisi. Un nom de connexion comme DOMAIN\bob ou a@b.com n’est pas un nom de dossier utilisable, et ce n’est pas quelque chose à placer dans un chemin. L’exemple utilise les 16 premiers caractères hexadécimaux du SHA-256 du nom en minuscules. Si vos utilisateurs ont un identifiant numérique dans votre base de données, renvoyez plutôt celui-ci.
RootFor ne lit que context.User : n’importe quelle connexion ASP.NET fonctionne donc, qu’il s’agisse de l’authentification Forms, de l’authentification Windows ou d’un cookie OWIN. L’exemple utilise l’authentification Forms avec une page de démonstration, SignIn.aspx, qui connecte n’importe quel nom saisi, sans mot de passe. Cette page répond 404 tant que le réglage n’est pas activé. Ne la déployez pas.
<!-- Any ASP.NET sign-in works (Forms, Windows, OWIN cookies): RootFor only reads
HttpContext.User. SignIn.aspx is a demo stand-in for your real login page. -->
<authentication mode="Forms">
<forms loginUrl="~/SignIn.aspx" name=".RTEDEMO" />
</authentication>Le 401 devenu 302
Le premier test avec le réglage activé n’a pas renvoyé de 401. Un GET /rte/gallery anonyme est revenu sous la forme d’un 302 vers /SignIn.aspx?ReturnUrl=%2frte%2fgallery…. FormsAuthenticationModule transforme chaque 401 en redirection vers loginUrl. Le XMLHttpRequest de la boîte de dialogue suit la redirection sans rien dire, reçoit le HTML de la page de connexion et signale une erreur. La correction tient en une ligne dans la fonction d’écriture du JSON, après quoi la même requête a répondu 401 {"error":"Sign in to use the image gallery."} :
public static void WriteJson(HttpContext context, int status, object value)
{
HttpResponse response = context.Response;
response.Clear();
response.StatusCode = status;
// Without this, IIS replaces 4xx/5xx bodies with its own error page when
// <httpErrors errorMode="Custom"> is in effect (the default for remote users).
response.TrySkipIisCustomErrors = true;
// FormsAuthenticationModule turns every 401 into a 302 to the login page, which an
// XMLHttpRequest follows silently and then fails to parse. Keep the 401.
response.SuppressFormsAuthenticationRedirect = true;
response.ContentType = "application/json";
response.Cache.SetCacheability(HttpCacheability.NoCache);
response.Cache.SetNoStore();
// JavaScriptSerializer ships with .NET Framework (System.Web.Extensions) and writes
// property names exactly as declared, so the anonymous objects above are camelCase.
// Its default MaxJsonLength (2,097,152 chars) would fail a huge folder; lift it.
JavaScriptSerializer serializer = new JavaScriptSerializer();
serializer.MaxJsonLength = int.MaxValue;
response.Write(serializer.Serialize(value));
}Les envois interrogent aussi RootFor en premier : un envoi anonyme reçoit donc un 401 plutôt que « No file was uploaded. ».
Ce que nous avons testé
Avec le réglage activé, un script s’est connecté en tant que alice et bob via SignIn.aspx, a conservé chaque cookie .RTEDEMO, et a constaté que :
- les envois d’alice sont allés dans
/uploads/users/2bd806c97f0e00af/…et ceux de bob dans/uploads/users/81b637d8fcd2c6da/…; - alice a créé
trips. Ce dossier figurait dans sa liste et pas dans celle de bob ; - pour alice,
folder=..a donné un 400 etfolder=usersun 404. Sa racine est son propre dossier : elle ne peut donc pas remonter dansusers/; - l’URL d’une image d’alice s’est chargée sans aucun cookie, avec le statut 200.
Ce dernier point est voulu. Les fichiers sous uploads/ sont des fichiers statiques qu’IIS sert à quiconque possède l’URL, et une image dans une page publiée doit se charger pour chaque lecteur. Les racines par utilisateur empêchent les utilisateurs d’accéder aux dossiers des autres dans la boîte de dialogue. Elles ne rendent pas les fichiers privés. Pour des fichiers privés, servez-les via un gestionnaire qui vérifie l’utilisateur, et gardez-les en dehors du dossier du site.
L’image ci-dessus montre uploads après les deux exécutions. Les dossiers partagés proviennent de l’exécution avec le réglage désactivé, et users/ contient un dossier par utilisateur connecté.
Tester
Dans le projet d’exemple de ce guide, réglez rte:PerUserFolders sur true et lancez run.ps1. Ensuite :
curl -i "http://localhost:5401/rte/gallery?folder="
HTTP/1.1 401 Unauthorized
{"error":"Sign in to use the image gallery."}Ouvrez /SignIn.aspx et connectez-vous avec n’importe quel nom. La page de démonstration indique qui vous êtes, et la galerie ouvre votre propre dossier, vide, sous uploads/users/. Remettez le réglage sur false : le test de contrat passe de nouveau (35 sur 35).
Le même guide pour d’autres plateformes
JavaScript · PHP · ASP.NET Core · MVC 5