Bildordner pro Benutzer in ASP.NET Web Forms

Der Ordner uploads des Web-Forms-Beispiels mit den gemeinsamen Ordnern blog, products und team und einem Ordner users mit zwei Ordnern pro Benutzer

Mit einem gemeinsamen Ordner uploads/ sieht jeder Benutzer des Editors in der Galerie die Bilder aller anderen Benutzer. Das Web-Forms-Beispiel ändert das mit einer Methode, RteFiles.RootFor, und einem Schalter in den appSettings. Ist der Schalter eingeschaltet, erhält jeder angemeldete Benutzer uploads/users/<id>/, und anonyme Requests erhalten ein 401. Diese Seite zeigt die Änderung, die Weiterleitung der Forms-Authentifizierung, die das 401 zunichtemacht, und was Ordner pro Benutzer nicht schützen.

Der Schalter

XML · web.config
<appSettings>
  <!-- true: each signed-in user gets uploads/users/<id>/ (see RteFiles.RootFor) -->
  <add key="rte:PerUserFolders" value="false" />
</appSettings>

Er ist standardmäßig ausgeschaltet, und dann verhält sich das Beispiel genau wie in der Galerie-Anleitung. Er wird mit WebConfigurationManager.AppSettings gelesen, wie jeder andere Wert in den appSettings.

RootFor

C# · App_Code/RteFiles.cs
// 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 gibt einen app-relativen virtuellen Pfad zurück. Alles andere baut darauf auf. Server.MapPath macht daraus den Stammordner auf der Festplatte, gegen den jeder Ordner geprüft wird, und VirtualPathUtility.ToAbsolute macht daraus das URL-Präfix jedes Bildes. Liste, Anlegen von Ordnern und Upload schalten also gemeinsam um. Der Ordner wird angelegt, wenn der Benutzer die Galerie zum ersten Mal öffnet.

Der Ordnername stammt aus HttpContext.User, aber nie so, wie er eingegeben wurde. Ein Anmeldename wie DOMAIN\bob oder a@b.com ist kein brauchbarer Ordnername und gehört nicht in einen Pfad. Das Beispiel verwendet die ersten 16 Hex-Zeichen des SHA-256 des kleingeschriebenen Namens. Wenn Ihre Benutzer in Ihrer Datenbank eine numerische ID haben, geben Sie stattdessen diese zurück.

RootFor liest nur context.User, daher funktioniert jede ASP.NET-Anmeldung: Forms-Authentifizierung, Windows-Authentifizierung oder ein OWIN-Cookie. Das Beispiel verwendet Forms-Authentifizierung mit einer Demoseite, SignIn.aspx, die jeden eingegebenen Namen ohne Passwort anmeldet. Diese Seite antwortet mit 404, solange der Schalter nicht eingeschaltet ist. Stellen Sie sie nicht bereit.

XML · web.config
<!-- 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>

Das 401, das zu einem 302 wurde

Der erste Test mit eingeschaltetem Schalter gab kein 401 zurück. Ein anonymes GET /rte/gallery kam als 302 auf /SignIn.aspx?ReturnUrl=%2frte%2fgallery… zurück. FormsAuthenticationModule macht aus jedem 401 eine Weiterleitung auf loginUrl. Der XMLHttpRequest des Dialogs folgt der Weiterleitung stillschweigend, erhält das HTML der Anmeldeseite und meldet einen Fehler. Die Lösung ist eine Zeile im JSON-Writer, danach antwortete derselbe Request mit 401 {"error":"Sign in to use the image gallery."}:

C# · App_Code/RteFiles.cs
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));
}

Auch Uploads fragen zuerst RootFor, sodass ein anonymer Upload ein 401 ist statt „No file was uploaded.“

Was wir getestet haben

Bei eingeschaltetem Schalter hat sich ein Skript über SignIn.aspx als alice und als bob angemeldet, jeweils das Cookie .RTEDEMO behalten und Folgendes festgestellt:

  • Alices Uploads landeten in /uploads/users/2bd806c97f0e00af/…, Bobs in /uploads/users/81b637d8fcd2c6da/….
  • Alice hat trips angelegt. Der Ordner stand in ihrer Liste und nicht in Bobs.
  • Für Alice ergab folder=.. ein 400 und folder=users ein 404. Ihr Stammordner ist ihr eigener Ordner, sie kann also nicht nach oben in users/ wechseln.
  • Die URL von Alices Bild ließ sich ganz ohne Cookie laden, mit Status 200.

Der letzte Punkt ist so gewollt. Dateien unter uploads/ sind statische Dateien, die IIS an jeden ausliefert, der die URL hat, und ein Bild auf einer veröffentlichten Seite muss für jeden Leser laden. Stammordner pro Benutzer halten die Benutzer im Dialog aus den Ordnern der anderen heraus. Sie machen die Dateien nicht privat. Für private Dateien liefern Sie diese über einen Handler aus, der den Benutzer prüft, und legen sie außerhalb des Website-Ordners ab.

Das Bild oben zeigt uploads nach beiden Läufen. Die gemeinsamen Ordner stammen aus dem Lauf mit ausgeschaltetem Schalter, und users/ enthält einen Ordner pro angemeldetem Benutzer.

Testen

Setzen Sie im Beispielprojekt zu dieser Anleitung rte:PerUserFolders auf true und starten Sie run.ps1. Dann:

Shell
curl -i "http://localhost:5401/rte/gallery?folder="
HTTP/1.1 401 Unauthorized
{"error":"Sign in to use the image gallery."}

Öffnen Sie /SignIn.aspx und melden Sie sich mit einem beliebigen Namen an. Die Demoseite zeigt an, wer Sie sind, und die Galerie öffnet Ihren eigenen, leeren Ordner unter uploads/users/. Stellen Sie den Schalter wieder auf false, und der Vertragstest besteht wieder (35 von 35).

Dieselbe Anleitung für andere Plattformen

JavaScript · PHP · ASP.NET Core · MVC 5

Weiterlesen