ASP.NET Core でのユーザーごとの画像フォルダー

ディスク上の ASP.NET Core のサンプルの uploads フォルダー。共有フォルダーと、alice と bob を含む users フォルダーがあり、それぞれに専用の写真がある

初期状態のギャラリーは全員に同じフォルダーを表示します。これは企業サイトには適しています。しかし、フォーラムや複数の顧客が使うアプリでは、各ユーザーが自分の画像だけを見られるようにすべきです。ASP.NET Core のサンプルでは、これはリクエストのファイルの置き場所を決める RootFor(HttpContext) という 1 つのメソッドで実現できます。このページでは、ユーザーごとのバージョン、それに必要なサインイン、そして公開されたままになるものを紹介します。

1 つのメソッドが決める

すべてのルート(一覧、フォルダーの作成、アップロード)は、RootFor にディスク上のフォルダーとその配信 URL を問い合わせます。コードのほかの部分は、ユーザーについて何も知りません。

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));
}
  • appsettings.json の PerUserFolders は、既定では false です。true に設定すると、サインインした各ユーザーのギャラリーのルートが uploads/users/<id>/ になります。「New folder」ボタンとアップロードはその中で動作し、フォルダーのチェックがこのルートに対して行われるため、.. でその外に出ることはできません。
  • ID は ClaimTypes.NameIdentifier です。ASP.NET Core Identity では、このクレームにユーザーの ID(変わることのない GUID)が入っています。ユーザー名やメールアドレスは変わる可能性があり、Windows では Alice と alice が 1 つのフォルダーになってしまいます。
  • ユーザーがいなければギャラリーもありません。匿名のリクエストには 401 {"error":"Sign in to use the image gallery."} が返り、ダイアログはサーバーのテキストではなく、独自の「The image gallery is unavailable right now (http-401).」を表示します。
  • ID はフォルダー名と同じようにチェックします。../../x のような ID は、パスにはならず 403 になります。

サインイン

サンプルでは、ASP.NET Core Identity もサインインに使う Cookie 認証を使っています。以下のテスト用サインインは、アプリ独自のログイン処理に置き換えてください。

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

ログインの仕組みを作らなくてもユーザーごとのフォルダーを試せるように、サンプルではテスト専用のサインインを、Development 環境でのみマップしています。これは誰でも任意のユーザーとしてサインインさせるものなので、実際のサーバーには決して置いてはいけません。

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

Production として実行される単純な dotnet run では、/demo/sign-in は 404 を返しました。

公開されたままになるもの

ギャラリーを分けても、ファイルが非公開になるわけではありません。/uploads/users/alice/harbor-fd5ff24b.jpg は、ほかのすべてのアップロードと同じ静的ファイルのマッピングで配信され、Cookie なしのリクエストにも 200 を返しました。公開コンテンツの画像であれば、通常はそれで問題ありません。投稿の読者がその画像を読み込めなければならないからです。ファイル名のランダムな部分により URL を推測されることはありませんが、URL を共有された人は誰でも開けます。ユーザーの画像を非公開にしておく必要がある場合は、UseStaticFiles ではなく、ユーザーをチェックするエンドポイントから配信してください。サンプルにはそのようなエンドポイントは含まれていません。

また、ユーザーごとのフォルダーを有効にしても、そもそも誰がアップロードできるかは決まりません。アプリにサインインの仕組みができたら、コントローラーに [Authorize] を、または最小 API のグループに .RequireAuthorization() を追加してください。

テストする

このガイドのサンプルプロジェクトを、ユーザーごとのフォルダーを有効にして Development 環境で起動し、2 つの Cookie ファイルを使って 2 人のユーザーとして操作します。

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

Bob のギャラリーには Bob の画像だけが表示されます。ブラウザーで http://localhost:5302/demo/sign-in?user=alice を開いて「Insert Gallery」をクリックすると、Alice の画像とフォルダーだけが表示されます。このページ上部の画像は、その後の uploads フォルダーです。PerUserFolders が無効なときにサンプルが使う共有フォルダーと並んで、users/alice と users/bob があります。

ほかのプラットフォーム向けの同じガイド

JavaScript · PHP · MVC 5 · Web Forms

次に読む