Dossiers d’images par utilisateur avec ASP.NET MVC 5

Dans l’exemple MVC 5, tous les utilisateurs de l’éditeur partagent un même dossier uploads/ tant que vous n’activez pas un réglage appSettings. Une fois celui-ci activé, les actions de galerie et d’envoi travaillent dans uploads/users/<id>/ pour l’utilisateur connecté et répondent aux requêtes anonymes par un 401 en JSON. Faire parvenir ce 401 jusqu’au navigateur demande une ligne de plus que prévu, et [Authorize] n’est pas le bon outil pour cela. Cette page explique pourquoi.
Le réglage et la racine
<!-- true: each signed-in user gets uploads/users/<id>/ (see RteFiles.RootFor) -->
<add key="rte:PerUserFolders" value="false" />Tout dans RteFiles part d’une seule méthode. Le contrôleur transmet son HttpContext (un HttpContextBase), et RootFor renvoie la racine relative à l’application pour cette requête :
// 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(HttpContextBase 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();
}
}La liste, la création de dossier et l’envoi résolvent tous leurs chemins à l’intérieur de cette racine, et les URL des images sont construites à partir d’elle. Rien d’autre ne change donc quand le réglage est activé. Le nom du dossier est dérivé de User.Identity.Name (16 caractères hexadécimaux de son SHA-256), et n’est jamais repris tel qu’il a été saisi. Si vos utilisateurs ont un identifiant en base de données, renvoyez plutôt celui-ci.
Pourquoi pas [Authorize]
[Authorize] sur RteController serait la façon évidente d’exiger une connexion. Avec l’authentification Forms, cela ne renvoie pas de 401 à la boîte de dialogue. Nous avons placé [Authorize] sur une action jetable et l’avons appelée de manière anonyme. La réponse a été un 302 vers /Account/SignIn?ReturnUrl=%2fProbe%2fSecret, avec ou sans en-tête X-Requested-With: XMLHttpRequest. [Authorize] renvoie un 401, et FormsAuthenticationModule transforme chaque 401 en redirection vers loginUrl. La requête de la boîte de dialogue suit cette redirection, reçoit le HTML de la page de connexion et ne parvient pas à l’analyser.
RootFor lève donc RteException(401, …), et la fonction utilitaire JSON du contrôleur conserve le code d’état :
// Json() refuses GET unless told otherwise (JsonRequestBehavior.DenyGet is the default)
// and caps output at 2,097,152 characters; both are lifted here. Property names are
// written as declared, so the anonymous objects in RteFiles produce camelCase JSON.
JsonResult JsonStatus(int status, object data)
{
Response.StatusCode = status;
// Without this, IIS replaces 4xx bodies with its own error page for remote users.
Response.TrySkipIisCustomErrors = true;
// Forms authentication would turn a 401 into a 302 to the login page; keep the 401.
Response.SuppressFormsAuthenticationRedirect = true;
Response.Cache.SetCacheability(HttpCacheability.NoCache);
Response.Cache.SetNoStore();
return new JsonResult
{
Data = data,
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
MaxJsonLength = int.MaxValue
};
}Avec SuppressFormsAuthenticationRedirect = true, un GET /rte/gallery anonyme a répondu 401 {"error":"Sign in to use the image gallery."}. Les envois appellent RootFor avant de lire le fichier : un envoi anonyme reçoit donc le même 401.
La connexion
RootFor ne lit que User : ASP.NET Identity avec les cookies OWIN, l’authentification Forms et l’authentification Windows fonctionnent donc toutes. L’exemple utilise l’authentification Forms :
<!-- Any ASP.NET sign-in works (Forms, Windows, OWIN cookies): RootFor only reads
HttpContext.User. AccountController.SignIn is a demo stand-in for your real login. -->
<authentication mode="Forms">
<forms loginUrl="~/Account/SignIn" name=".RTEDEMO" />
</authentication>AccountController.SignIn est une démo qui connecte n’importe quel nom saisi, sans mot de passe. Son POST porte [ValidateAntiForgeryToken], et tout le contrôleur répond 404 tant que le réglage n’est pas activé. Remplacez-le par votre vraie page de connexion. Si vous utilisez plutôt l’authentification par cookie OWIN, sa propre redirection des 401 demande la même attention. Nous n’avons pas testé cette combinaison.
Ce que nous avons testé
Avec le réglage activé, un script a récupéré le formulaire de connexion, l’a envoyé avec le jeton antifalsification pour alice et pour bob, puis a utilisé leurs cookies .RTEDEMO :
- Les fichiers d’alice sont allés dans
/uploads/users/2bd806c97f0e00af/et ceux de bob dans/uploads/users/81b637d8fcd2c6da/. - Le dossier
tripsd’alice est apparu dans sa liste et pas dans celle de bob. folder=..a donné un 400.folder=usersa donné un 404, car la racine d’un utilisateur est son propre dossier.- L’URL d’une image d’alice a renvoyé 200 à une requête sans cookie.
Ce dernier résultat est normal. /uploads/ est servi par IIS en fichiers statiques : toute personne qui a l’URL peut charger l’image, ce dont un article publié a besoin. Les dossiers par utilisateur séparent les utilisateurs dans la boîte de dialogue. Ils ne rendent pas les fichiers privés.
L’image en haut de la page montre le dossier uploads de l’exemple après les tests : les dossiers partagés d’une exécution avec le réglage désactivé, et users/ avec un dossier par utilisateur.
Tester
Dans le projet d’exemple de ce guide, réglez rte:PerUserFolders sur true et lancez run.ps1 (port 5402) :
curl -i "http://localhost:5402/rte/gallery?folder="
HTTP/1.1 401 Unauthorized
{"error":"Sign in to use the image gallery."}Ouvrez ensuite /Account/SignIn, connectez-vous et ouvrez Insert Gallery : la galerie s’ouvre dans votre propre dossier, vide. Avec le réglage remis à false, le test de contrat passe 35 vérifications sur 35.
Le même guide pour d’autres plateformes
JavaScript · PHP · ASP.NET Core · Web Forms