Pastas de imagens por usuário no ASP.NET Core

Por padrão, a galeria mostra as mesmas pastas para todo mundo, o que serve para um site de empresa. Em um fórum ou em um aplicativo com vários clientes, cada pessoa só deve ver as próprias imagens. Nos exemplos ASP.NET Core, isso é um único método, RootFor(HttpContext), que decide onde ficam os arquivos de uma requisição. Esta página mostra a versão por usuário, o login de que ela precisa e o que continua público.
Um método decide
Toda rota (listar, criar pasta, upload) pede a RootFor uma pasta no disco e a URL em que ela é servida. Nenhuma outra parte do código sabe nada sobre usuários.
// 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));
}PerUserFoldersemappsettings.jsonéfalsepor padrão. Comtrue, cada usuário autenticado recebeuploads/users/<id>/como raiz da galeria. O botão New folder e os uploads funcionam dentro dela, e..não consegue sair dela, porque a verificação de pasta é feita em relação a essa raiz.- O id é
ClaimTypes.NameIdentifier. Com o ASP.NET Core Identity, essa claim contém o id do usuário, um GUID, que nunca muda. Um nome de usuário ou endereço de e-mail pode mudar, e no WindowsAliceealiceseriam a mesma pasta. - Sem usuário, sem galeria. Uma requisição anônima recebe
401 {"error":"Sign in to use the image gallery."}, e a caixa de diálogo mostra sua própria mensagem, "The image gallery is unavailable right now (http-401).", em vez do texto do servidor. - O id é verificado como um nome de pasta. Um id como
../../xrecebe um 403 em vez de um caminho.
Login
O exemplo usa autenticação por cookie, o mesmo esquema com que o ASP.NET Core Identity faz login. O login do seu próprio aplicativo substitui o login de teste abaixo.
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] attributesPara testar as pastas por usuário sem criar um login, o exemplo mapeia um login só para testes, e apenas no ambiente Development. Ele autentica qualquer pessoa como qualquer usuário, então nunca pode chegar a um servidor real:
// 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("/");
});
}Com um simples dotnet run, que roda como Production, /demo/sign-in respondeu 404.
O que continua público
Galerias separadas não tornam os arquivos privados. /uploads/users/alice/harbor-fd5ff24b.jpg é servido pelo mesmo mapeamento de arquivos estáticos que qualquer outro upload, e respondeu 200 a uma requisição sem cookie. Isso costuma ser o certo para imagens em conteúdo publicado: quem lê um post precisa conseguir carregar as imagens dele. A parte aleatória do nome do arquivo impede que alguém adivinhe a URL, mas qualquer pessoa com quem ela for compartilhada consegue abri-la. Se as imagens de um usuário precisam ficar privadas, sirva-as por um endpoint que verifique o usuário, e não por UseStaticFiles. O exemplo não inclui um.
Ativar as pastas por usuário também não decide quem pode fazer upload. Adicione [Authorize] ao controller, ou .RequireAuthorization() ao grupo da Minimal API, quando o aplicativo tiver login.
Teste
Inicie o projeto de exemplo deste guia em Development com as pastas por usuário ativadas e trabalhe como dois usuários, com dois arquivos de cookies:
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}]}A galeria de Bob lista só a imagem dele. No navegador, abra http://localhost:5302/demo/sign-in?user=alice e clique em Insert Gallery: ela mostra apenas as imagens e pastas de Alice. A imagem no topo desta página é a pasta uploads depois disso, com users/alice e users/bob ao lado das pastas compartilhadas que o exemplo usa quando PerUserFolders está desativado.
O mesmo guia para outras plataformas
JavaScript · PHP · MVC 5 · Web Forms