ASP.NET MVC 5 中每个用户的图片文件夹

在 MVC 5 示例中,所有编辑器用户共享同一个 uploads/ 文件夹,直到您打开一个 appSettings 开关。打开后,图片库和上传操作方法会为已登录用户在 uploads/users/<id>/ 中工作,并以 JSON 401 响应匿名请求。要把这个 401 送到浏览器,需要比预想中多写一行代码,而且 [Authorize] 并不适合做这件事。本页说明原因。
开关与根目录
<!-- true: each signed-in user gets uploads/users/<id>/ (see RteFiles.RootFor) -->
<add key="rte:PerUserFolders" value="false" />RteFiles 中的一切都从一个方法开始。控制器传入它的 HttpContext(一个 HttpContextBase),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(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();
}
}列表、创建文件夹和上传都在这个根目录内解析路径,图片 URL 也由它构造。因此打开开关后,其他部分都不需要改动。文件夹名称由 User.Identity.Name 派生(取其 SHA-256 的 16 个十六进制字符),绝不直接使用用户输入的原样名称。如果您的用户有数据库 id,请改为返回该 id。
为什么不用 [Authorize]
在 RteController 上加 [Authorize] 看起来是要求登录的直接方法。但在 Forms 身份验证下,它不会给对话框返回 401。我们在一个临时操作方法上加了 [Authorize] 并匿名请求它。无论是否带有 X-Requested-With: XMLHttpRequest 标头,响应都是重定向到 /Account/SignIn?ReturnUrl=%2fProbe%2fSecret 的 302。[Authorize] 返回 401,而 FormsAuthenticationModule 会把每个 401 改写为重定向到 loginUrl。对话框的请求跟随重定向,拿到登录页面的 HTML,然后解析失败。
因此 RootFor 抛出 RteException(401, …),并由控制器的 JSON 辅助方法保留这个状态码:
// 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
};
}设置 SuppressFormsAuthenticationRedirect = true 后,匿名的 GET /rte/gallery 返回 401 {"error":"Sign in to use the image gallery."}。上传会在读取文件之前调用 RootFor,因此匿名上传也会得到同样的 401。
登录
RootFor 只读取 User,因此基于 OWIN Cookie 的 ASP.NET Identity、Forms 身份验证和 Windows 身份验证都能使用。示例使用 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 是一个演示,它无需密码,输入任何名称都能登录。它的 POST 带有 [ValidateAntiForgeryToken],并且除非打开开关,否则整个控制器都返回 404。请把它替换为您真正的登录功能。如果改用 OWIN Cookie 身份验证,它自己对 401 的重定向也需要同样处理。我们没有测试这种组合。
我们测试了什么
在开关打开的情况下,一个脚本获取登录表单,分别以 alice 和 bob 带着防伪造令牌提交,并使用各自的 .RTEDEMO Cookie:
- alice 的文件保存到
/uploads/users/2bd806c97f0e00af/,bob 的保存到/uploads/users/81b637d8fcd2c6da/。 - alice 的
trips文件夹出现在她的列表中,而没有出现在 bob 的列表中。 folder=..得到 400。folder=users得到 404,因为用户的根目录就是他们自己的文件夹。- alice 的图片 URL 对不带 Cookie 的请求返回了 200。
最后一个结果在意料之中。/uploads/ 由 IIS 作为静态文件提供,因此任何拿到 URL 的人都能加载图片,这正是已发布文章所需要的。每个用户的文件夹在对话框中把用户分隔开,但并不会让文件变成私有的。
顶部的图片是测试之后示例的 uploads 文件夹:其中有开关关闭时运行留下的共享文件夹,以及每个用户一个文件夹的 users/。
测试
在本指南的示例项目中,把 rte:PerUserFolders 设置为 true,然后启动 run.ps1(端口 5402):
curl -i "http://localhost:5402/rte/gallery?folder="
HTTP/1.1 401 Unauthorized
{"error":"Sign in to use the image gallery."}然后打开 /Account/SignIn 登录,再打开 Insert Gallery:它会从您自己的空文件夹开始。把开关改回 false 后,契约测试通过全部 35 项中的 35 项。
其他平台的同一指南
JavaScript · PHP · ASP.NET Core · Web Forms