翻译 - ASP.NET Core 基本知识 - 静态文件(Static Files)


翻译自 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/static-files?view=aspnetcore-5.0

静态文件,例如 HTML, CSS,images 和 JavaScript,都是作为资源文件由 ASP.NET Core 应用程序默认的直接提供给客户端。

服务静态文件

静态文件存储在项目的 web root 目录。默认目录是 {contentroot}/wwwroot,但是可以使用 UseWebRoot 方法更改。更多信息,查看 Content root 和 Web root。

CreateDefaultBuilder 方法设置内容根目录为当前目录:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup();
            });
}

上面的代码使用 web 应用程序模板创建。

静态文件可以通过相对于 web root 的路径获取。例如,Web 应用程序项目模板包含几个文件夹在 wwwroot 中:

wwwroot:

  • css
  • js
  • lib

考虑床架你 wwwroot/images 文件夹,添加 wwwroot/images/MyImage.jpg 文件到该目录。访问一个 Images 文件夹中的文件的 URI 格式是 https:///images/。例如,https://localhost:5001/images/MyImage.jpg。

在 web root 中服务文件

默认的 web 应用程序模板在 Startup.Configure 中调用 UseStaticFiles  方法,这使能了静态文件可以被服务:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();

    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

无参的 UseStaticFiles 方法标记了 web root 中的文件可以被服务。下面的标记引用了 wwwroot/images/MyImage.jpg:

"~/images/MyImage.jpg" class="img" alt="My image" />

在上面的代码中,波浪 ~/ 指向 web root。

服务 web root 之外的文件

考虑一个目录层级,在这个目录中静态文件被服务于 web root 之外:

wwwroot:

  • css
  • js
  • images

MyStaticsFiles:

  • images
    - red-rose.jpg

通过以下配置静态文件中间件,一个请求可以访问 red-rose.jpg 文件:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();

    // using Microsoft.Extensions.FileProviders;
    // using System.IO;
    app.UseStaticFiles(new StaticFileOptions
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(env.ContentRootPath, "MyStaticFiles")),
        RequestPath = "/StaticFiles"
    });

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

在上面的代码中,MyStaticFiles 目录通过 StaticFiles URI 段公开暴露。指向 https:///StaticFiles/images/red-rose.jpg 服务 red-rose.jpg 文件。

下面的标记指向 MyStaticFiles/images/red-rose.jpg:

"~/StaticFiles/images/red-rose.jpg" class="img" alt="A red rose" />

设置 HTTP 相应头部

一个 StaticFileOptions 对象可用于设置 HTTP 相应头部。除了设置从 web root 服务静态文件外,下面的代码也设置了 Cache-Control 头部信息:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();

    const string cacheMaxAge = "604800";
    app.UseStaticFiles(new StaticFileOptions
    {
        OnPrepareResponse = ctx =>
        {
            // using Microsoft.AspNetCore.Http;
            ctx.Context.Response.Headers.Append(
                 "Cache-Control", $"public, max-age={cacheMaxAge}");
        }
    });

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

静态文件公开缓存的时间是 600 秒:

 静态文件授权

ASP.NET Core 模板在调用 UseAuthorization 之前调用 UseStaticFiles。大部分的应用程序遵循这个模式。当静态文件中间件在授权中间件之前调用:

  • 没有对静态文件执行授权检查
  • 静态文件由静态文件中间件服务,例如在 wwwroot 下,是可以公开访问的

基于授权的服务静态文件:

  • 保存它们在 wwwroot 之外
  • 在 UseAuthorization 之后调用 UseStaticFiles 指定一个路径
  • 设置 fallback authorization policy
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();

    // wwwroot css, JavaScript, and images don't require authentication.
    app.UseStaticFiles();   

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseStaticFiles(new StaticFileOptions
    {
        FileProvider = new PhysicalFileProvider(
                     Path.Combine(env.ContentRootPath, "MyStaticFiles")),
        RequestPath = "/StaticFiles"
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection")));
        services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores();

        services.AddRazorPages();

        services.AddAuthorization(options =>
        {
            options.FallbackPolicy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();
        });
    }

    // Remaining code ommitted for brevity.

在上面的代码中,fallback authorization policy 要求所有的用户都要认证。像 controller, Razore Pages 等等指定了它们自己授权要求的 Endpoints 不会使用 fallback authorization 策略。例如, Razor Pages, controllers 或者使用 [AllowAnonymous] 或 [Authorize(PolicyNAme="MyPolicy")] 会使用属性授权而不是 fallback authorization 策略。

RequireAuthenticatedUser 添加 DenyAnonymousAuthorizationRequirement 到当前实例,这会强制当前用户被认证。

在 wwwroot 下面的静态资源文件是公开可以访问的,因为默认的静态文件中间件 (app.UseStaticFiles();) 在 UseAuthentication 之前调用。MyStaticFiles 中的静态资源要求认证。 sample code 展示了这些。

一种替代的基于授权的方法服务文件是:

  • 存储文件在 wwwroot 外部,任意的可以由静态文件中间件访问到的目录
  • 通过一个需要授权的方法服务它们,返回一个 FileResult 对象
[Authorize]
public IActionResult BannerImage()
{
    var filePath = Path.Combine(
        _env.ContentRootPath, "MyStaticFiles", "images", "red-rose.jpg");

    return PhysicalFile(filePath, "image/jpeg");
}

目录浏览

目录浏览允许目录列出指定目录的文件。

默认的由于安全的原因,目录浏览是关闭的。更多信息查看 Considerations。

使用以下方法打开目录浏览:

  • 在 Startup.ConfigureServies 调用 AddDirectoryBrowser
  • 在 Startup.Configure 中调用 UseDirectoryBrowser 
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddDirectoryBrowser();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();

    // using Microsoft.Extensions.FileProviders;
    // using System.IO;
    app.UseStaticFiles(new StaticFileOptions
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(env.WebRootPath, "images")),
        RequestPath = "/MyImages"
    });

    app.UseDirectoryBrowser(new DirectoryBrowserOptions
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(env.WebRootPath, "images")),
        RequestPath = "/MyImages"
    });

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

上面的代码允许浏览目录 wwwroot/images 使用 URL https:///MyImages,使用链接访问每一个文件和目录:

 服务默认的文档

为访客在站点开始的地方提供一个默认的页面。从 wwwroot 提供一个默认的页面而无需一个完整的 URI,调用 UseDefaultFiles:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();

    app.UseDefaultFiles();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

UseDefaultFiles 必须在 UseStaticFiles 之前调用。UseDefaultFiles 是一个 URL 的重写,它并不服务文件。

使用 UseDefaultFiles,请求 wwwroot 的一个目录的话,会搜索以下文件:

  • default.htm
  • default.html
  • index.htm
  • index.html

从列表中找到的第一个文件被作为服务的文件,就好像请求的是完整的指定的 URI。浏览器的 URL 反映了 URI 请求。

下面的代码改变了默认文件的名称为 mydefault.html:

var options = new DefaultFilesOptions();
options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("mydefault.html");
app.UseDefaultFiles(options);
app.UseStaticFiles();

下面的代码使用了上面的代码在 Startup.Configure:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();

    var options = new DefaultFilesOptions();
    options.DefaultFileNames.Clear();
    options.DefaultFileNames.Add("mydefault.html");
    app.UseDefaultFiles(options);
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

为默认文档 UseFileServer

UseFileServer 结合了 UseStaticFiles,UseDefaultFiles 和可选的 UseDirectoryBrowder 的功能。

调用 app.UseFileServer 使能了服务静态文件和默认文件。目录浏览并没有打开。下面的代码展示了在 Startup.Configure 中使用 UseFileServer:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();

    app.UseFileServer();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

下面的代码使能了服务静态文件,默认文件,和目录浏览:

app.UseFileServer(enableDirectoryBrowsing: true);

下面的代码展示了在 Startup.Configure 中使用以上代码:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();

    app.UseFileServer(enableDirectoryBrowsing: true);

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

考虑下面的目录层级:

wwwroot:

 - css

 - images

 - js

MyStaticFiles:

 -images

   - MyImage.jpg

 - default.html

下面的代码使能了 MyStaticFiles 的服务静态文件,默认文件和目录浏览:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddDirectoryBrowser();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();

    app.UseStaticFiles(); // For the wwwroot folder.

    // using Microsoft.Extensions.FileProviders;
    // using System.IO;
    app.UseFileServer(new FileServerOptions
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(env.ContentRootPath, "MyStaticFiles")),
        RequestPath = "/StaticFiles",
        EnableDirectoryBrowsing = true
    });

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

当 EnableDirectoryBrowsing 属性为 true 时 AddDirectoryBrowser 必须被调用。

使用文件层级和上面的代码, URLs 解析如下:

URI Response
https:///StaticFiles/images/MyImage.jpg MyStaticFiles/images/MyImage.jpg
https:///StaticFiles MuStaticFiles/default.html

如果没有 default-named 的文件存在于 MyStaticFiles 目录,https:///StaticFiles 返回可点击的目录列表:

 UseDefaultFiles 和 UseDirectoryBrowser 执行了从没有尾部 / 的目标 URI 到带有尾部 / 的目标 URI。例如,从 https:///StaticFiles 到 https:///StaticFiles/。没有尾部斜杠(/) 的 StaticFiles 目录的相对的 URLs 是无效的。

FileExtensionContentTypeProvider

FileExtensionContentTypeProvider 类包含了一个 Mappings 属性,用来服务作为一个映射文件扩展到 MIME 内容类型。下面的例子中,服务文件扩展被映射到一直的 MIME 类型。.rtf 扩展被替换,.mp4 被移除:

// using Microsoft.AspNetCore.StaticFiles;
// using Microsoft.Extensions.FileProviders;
// using System.IO;

// Set up custom content types - associating file extension to MIME type
var provider = new FileExtensionContentTypeProvider();
// Add new mappings
provider.Mappings[".myapp"] = "application/x-msdownload";
provider.Mappings[".htm3"] = "text/html";
provider.Mappings[".image"] = "image/png";
// Replace an existing mapping
provider.Mappings[".rtf"] = "application/x-msdownload";
// Remove MP4 videos.
provider.Mappings.Remove(".mp4");

app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(env.WebRootPath, "images")),
    RequestPath = "/MyImages",
    ContentTypeProvider = provider
});

app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(env.WebRootPath, "images")),
    RequestPath = "/MyImages"
});

下面的代码展示了在 Startup.Confiure 中使用上面的代码:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();

    // using Microsoft.AspNetCore.StaticFiles;
    // using Microsoft.Extensions.FileProviders;
    // using System.IO;

    // Set up custom content types - associating file extension to MIME type
    var provider = new FileExtensionContentTypeProvider();
    // Add new mappings
    provider.Mappings[".myapp"] = "application/x-msdownload";
    provider.Mappings[".htm3"] = "text/html";
    provider.Mappings[".image"] = "image/png";
    // Replace an existing mapping
    provider.Mappings[".rtf"] = "application/x-msdownload";
    // Remove MP4 videos.
    provider.Mappings.Remove(".mp4");

    app.UseStaticFiles(new StaticFileOptions
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(env.WebRootPath, "images")),
        RequestPath = "/MyImages",
        ContentTypeProvider = provider
    });

    app.UseDirectoryBrowser(new DirectoryBrowserOptions
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(env.WebRootPath, "images")),
        RequestPath = "/MyImages"
    });

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

查看  MIME content types。

Non-standard content types

静态文件中间件能够识别将近 400 个已知的文件内容类型。如果用户请求的一个未知的文件类型,静态文件中间件会传递请求到管道中的下一个中间件。如果没有中间件处理请求,会返回一个 404 Not Found 响应。如果目录浏览打开了,文件的链接会显示到一个目录列表中。

下面的代码使能了服务未知文件类型,并且渲染未知文件为一个图片:

app.UseStaticFiles(new StaticFileOptions
{
    ServeUnknownFileTypes = true,
    DefaultContentType = "image/png"
});

下面的代码展示了在 Startup.Cofigure 中使用前面的代码:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();

    app.UseStaticFiles(new StaticFileOptions
    {
        ServeUnknownFileTypes = true,
        DefaultContentType = "image/png"
    });

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

使用上面的代码,请求一个未知内容类型的文件会返回一个图片。

警告:

使能 ServeUnknownFileTypes 是一个安全风险。默认是禁用的,并且不鼓励使用。FileExtensionContentTypeProvider 提供一个安全的可替代的方法使用 non-standard 扩展服务文件。

Serve files from multiple locations

UseStaticFiles 和 UseFileServer 默认的文件提供器都指向 wwwroot。另外,UseStaticFiles 和 UseFileServer 的实例可以使用其它文件提供器从其它位置服务文件。更多信息,查看  this GitHub issue。

Security considerations for static files

警告:

UseDirectoryBrowser 和 UseStaticFiles 可能会导致泄漏秘密。强烈推荐在生产环境中禁用目录浏览。细心的检查哪些目录通过 UseStaticFiles 或者 UseDirectoryBrowser 开启了。整个目录和它的子目录都变的公开可访问了。公开服务的文件应该存储到一个专用的目录中,例如 /wwwroot。把这些文件和 MVC views,Razor Pages,配置文件等等隔离开。

  • 使用 UseDirectoryBrowser 和 UseStaticFiles 暴露的内容的 URLs 受制于文件系统大小写敏感和字符限制。例如,Windows 是大小写敏感的,但是 macOS 和 Linux 并不是
  • 在 IIS 中托管的 ASP.NET Core 应用程序使用 ASP.NET Core Module 转发所有的请求到应用程序,包括应该文件请求。IIS 静态文件处理程序没有被使用,并没有机会处理请求
  • 在 IIS 管理器中完成下面的步骤来以服务器或者 website 级别移除 IIS 静态文件程序
    1. 导航到模块特性
    2. 选择列表中的 StaticFileModule
    3. 在 Actions 侧边栏中点击 Remove

警告:

如果 IIS 静态文件处理程序开启了,并且 ASP.NET Core 模块没有正确配置,静态文件也会被服务。例如,如果 web.config 没有部署的话就会发生这种情况。

  •  把代码文件,包括 .cs 和 .cshtml 文件放置在项目 web root 之外。一个应用程序客户端内容和服务端代码中间的逻辑的隔离因此会被创建。这能够阻止服务端代码的泄漏。