虽然ASP.NET Core是一款“动态”的Web服务端框架,但是由它接收并处理的大部分是针对静态文件的请求,最常见的是开发Web站点使用的3种静态文件(JavaScript脚本、CSS样式和图片)。ASP.NET Core提供了3个中间件来处理针对静态文件的请求,利用它们不仅可以将物理文件发布为可以通过HTTP请求获取的Web资源,还可以将所在的物理目录的结构呈现出来。通过HTTP请求获取的Web资源大部分来源于存储在服务器磁盘上的静态文件。对于ASP.NET Core应用来说,如果将静态文件存储到约定的目录下,绝大部分文件类型都是可以通过Web的形式对外发布的。基于静态文件的请求由3个中间件负责处理,它们均定义在NuGet包“Microsoft.AspNetCore.StaticFiles”中,利用这3个中间件完全可以搭建一个基于Web的文件服务器,下面做相关的实例演示。[更多关于ASP.NET Core的文章请点这里]
目录
一、发布物理文件
二、呈现目录结构
三、显示默认页面
四、映射媒体类型
一、发布物理文件
我们创建的演示实例是一个简单的ASP.NET Core应用,它的项目结构如下图所示。在默认作为WebRoot的“wwwroot”目录下,可以将JavaScript脚本文件、CSS样式文件和图片文件存放到对应的子目录(js、css和img)下。WebRoot目录下的所有文件将自动发布为Web资源,客户端可以访问相应的URL来读取对应文件的内容。
针对具体某个静态文件的请求是通过一个名为StaticFileMiddleware的中间件来处理的。如下面的代码片段所示,承载ASP.NET Core应用的程序中调用IApplicationBuilder接口的UseStaticFiles扩展方法注册的就是这样一个中间件。
public class Program { public static void Main() { Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(builder=>builder.Configure(app => app.UseStaticFiles())) .Build() .Run(); } }
上述程序运行之后,就可以通过GET请求的方式来读取对应文件的内容。请求采用的URL由目标文件的路径决定。具体来说,目标文件相对于WebRoot目录的路径就是对应URL的路径,如JPG图片文件“~/wwwroot/img/dolphin1.jpg”对应的URL路径为“/img/dolphin1.jpg”。如果直接利用浏览器访问这个URL,目标图片就会直接以下图所示的形式显示出来。
上面通过一个简单的实例将WebRoot所在目录下的所有静态文件发布为Web资源,如果需要发布的静态文件存储在其他目录下呢?下面将上面演示的应用程序的一些文档存储在下图所示的“~/doc/”目录下,那么对应的程序又该如何编写?
ASP.NET Core应用在大部分情况下都是利用一个IFileProvider对象来读取文件的针对静态文件的读取请求也不例外。对于IApplicationBuilder接口的UseStaticFiles扩展方法注册的StaticFileMiddleware中间件来说,它的内部维护着一个IFileProvider对象和请求路径的映射关系。如果调用UseStaticFiles方法没有指定任何参数,那么这个映射关系的请求路径就是应用的基地址(PathBase),对应的IFileProvider对象自然就是指向WebRoot目录的PhysicalFileProvider对象。
上述需求可以通过显式定制这个映射关系的方式来实现。如下面的代码片段所示,我们在现有程序的基础上额外添加了一次针对UseStaticFiles扩展方法的调用,在本次调用中指定一个对应的Options对象(一个类型为StaticFileOptions的对象)作为参数来定制请求路径(“/documents”)与对应IFileProvider对象(针对路径“~/doc/”的PhysicalFileProvider对象)之间的映射关系。
public class Program { public static void Main() { var path = Path.Combine(Directory.GetCurrentDirectory(), "doc"); var options = new StaticFileOptions { FileProvider = new PhysicalFileProvider(path), RequestPath = "/documents" }; Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(builder => builder.Configure(app => app .UseStaticFiles() .UseStaticFiles(options))) .Build() .Run(); } }
按照上面这段程序指定的映射关系,对于存储在“~/doc/”目录下的这个PDF文件(checklist.pdf),对应URL的路径就应该是“/documents/checklist.pdf”。如果利用浏览器请求这个地址时,PDF文件的内容就会按照下图所示的形式显示在浏览器上。
二、呈现目录结构
上面的演示实例注册的StaticFileMiddleware中间件只会处理针对具体的某个静态文件的请求,如果利用浏览器发送一个针对目录的请求(如“http://localhost:5000/img/”),得到的将是一个状态为“404 Not Found”的响应。如果希望浏览器呈现出目标目录的结构,就可以注册另一个名为DirectoryBrowserMiddleware的中间件。这个中间件会返回一个HTML页面,请求目录下的结构会以表格的形式显示在这个页面中。我们演示的程序可以按照如下方式调用IApplicationBuilder接口的UseDirectoryBrowser扩展方法来注册DirectoryBrowserMiddleware中间件。
public class Program { public static void Main() { var path = Path.Combine(Directory.GetCurrentDirectory(), "doc"); var fileProvider = new PhysicalFileProvider(path); var fileOptions = new StaticFileOptions { FileProvider = fileProvider, RequestPath = "/documents" }; var diretoryOptions = new DirectoryBrowserOptions { FileProvider = fileProvider, RequestPath = "/documents" }; Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(builder => builder.Configure(app => app .UseStaticFiles() .UseStaticFiles(fileOptions) .UseDirectoryBrowser() .UseDirectoryBrowser(diretoryOptions))) .Build() .Run(); } }
当上面的应用启动之后,如果利用浏览器向针对某个目录的URL(如“http://localhost:5000/”或者“http://localhost:5000/img/”)发起请求,目标目录的内容(包括子目录和文件)就会以图14-5所示的形式显示在一个表格中。可以看出,在呈现的表格中,当前目录的子目录和文件均会显示为链接。
三、显示默认页面
从安全的角度来讲,利用注册的UseDirectoryBrowser中间件会将整个目标目录的结构和所有文件全部暴露出来,所以这个中间件需要根据自身的安全策略谨慎使用。对于针对目录的请求,更加常用的处理策略就是显示一个保存在这个目录下的默认页面。默认页面文件一般采用如下4种命名约定:default.htm、default.html、index.htm和index.html。针对默认页面的呈现实现在一个名为DefaultFilesMiddleware的中间件中,我们演示的这个应用就可以按照如下方式调用IApplicationBuilder接口的UseDefaultFiles扩展方法来注册这个中间件。
public class Program { public static void Main() { var path = Path.Combine(Directory.GetCurrentDirectory(), "doc"); var fileProvider = new PhysicalFileProvider(path); var fileOptions = new StaticFileOptions { FileProvider = fileProvider, RequestPath = "/documents" }; var diretoryOptions = new DirectoryBrowserOptions { FileProvider = fileProvider, RequestPath = "/documents" }; var defaultOptions = new DefaultFilesOptions { RequestPath = "/documents", FileProvider = fileProvider, }; Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(builder => builder.Configure(app => app .UseDefaultFiles() .UseDefaultFiles(defaultOptions) .UseStaticFiles() .UseStaticFiles(fileOptions) .UseDirectoryBrowser() .UseDirectoryBrowser(diretoryOptions))) .Build() .Run(); } }
下面在“~/wwwroot/img/”目录和“~/doc”目录下分别创建一个名为index.html的默认页面,并且在该.html文件的主体部分指定一段简短的文字(This is an index page!)。在应用启动之后,可以利用浏览器访问这两个目录对应的URL(“http://localhost:5000/img/”和“http://localhost:5000/documents/”),下图显示的就是这个默认页面的内容。
必须在注册StaticFileMiddleware中间件和DirectoryBrowserMiddleware中间件之前注册DefaultFilesMiddleware中间件,否则它无法发挥作用。这是因为DirectoryBrowserMiddleware中间件和DefaultFilesMiddleware中间件处理的均是针对目录的请求,如果先注册DirectoryBrowserMiddleware中间件,那么显示的总是目录的结构;如果先注册用于显示默认页面的DefaultFilesMiddleware中间件,那么在默认页面不存在的情况下它会将请求分发给后续中间件,而DirectoryBrowserMiddleware中间件会接收请求的处理并将当前目录的结构呈现出来。
要先于StaticFileMiddleware中间件之前注册DefaultFilesMiddleware中间件是因为后者是通过采用URL重写的方式实现的,也就是说,这个中间件会将针对目录的请求改写成针对默认页面的请求,而最终针对默认页面的请求还需要依赖StaticFileMiddleware中间件来完成。DefaultFilesMiddleware中间件在默认情况下总是以约定的名称(default.htm、default.html、index.htm和index.html)在当前请求的目录下定位默认页面。如果作为默认页面的文件没有采用这样的约定命名(如我们将默认页面命名为readme.html),就需要按照如下方式显式指定默认页面的文件名。
public class Program { public static void Main() { var path = Path.Combine(Directory.GetCurrentDirectory(), "doc"); var fileProvider = new PhysicalFileProvider(path); var fileOptions = new StaticFileOptions { FileProvider = fileProvider, RequestPath = "/documents" }; var diretoryOptions = new DirectoryBrowserOptions { FileProvider = fileProvider, RequestPath = "/documents" }; var defaultOptions1 = new DefaultFilesOptions(); var defaultOptions2 = new DefaultFilesOptions { RequestPath = "/documents", FileProvider = fileProvider, }; defaultOptions1.DefaultFileNames.Add("readme.html"); defaultOptions2.DefaultFileNames.Add("readme.html"); Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(builder => builder.Configure(app => app .UseDefaultFiles(defaultOptions1) .UseDefaultFiles(defaultOptions2) .UseStaticFiles() .UseStaticFiles(fileOptions) .UseDirectoryBrowser() .UseDirectoryBrowser(diretoryOptions))) .Build() .Run(); } }
四、映射媒体类型
通过上面演示的实例可以看出,浏览器能够准确地将请求的目标文件的内容正常呈现出来。对HTTP协议具有基本了解的读者应该都知道:响应文件能够在浏览器上被正常显示的基本前提是响应报文通过Content-Type报头携带的媒体类型必须与内容一致。我们的实例演示了针对两种文件类型的请求,一种是JPG文件,另一种是PDF文件,对应的媒体类型分别是image/jpg和application/pdf,那么用来处理静态文件请求的StaticFileMiddleware中间件是如何解析出对应的媒体类型的?
StaticFileMiddleware中间件针对媒体类型的解析是通过一个IContentTypeProvider对象来完成的,默认采用的是该接口的实现类型FileExtensionContentTypeProvider。顾名思义,FileExtensionContentTypeProvider根据文件的扩展命名来解析媒体类型。FileExtensionContentTypeProvider内部预定了数百种常用文件扩展名与对应媒体类型之间的映射关系,所以如果发布的静态文件具有标准的扩展名,那么StaticFileMiddleware中间件就能为对应的响应赋予正确的媒体类型。
如果某个文件的扩展名没有在预定义的映射之中,或者需要某个预定义的扩展名匹配不同的媒体类型,那么应该如何解决?同样是针对我们演示的这个实例,笔者将~/wwwroot/img/ dolphin1.jpg文件的扩展名改成.img,毫无疑问,StaticFileMiddleware中间件将无法为针对该文件的请求解析出正确的媒体类型。这个问题具有若干不同的解决方案,第一种方案就是按照如下方式让StaticFileMiddleware中间件支持不能识别的文件类型,并为它们设置一个默认的媒体类型。
public class Program { public static void Main() { var options = new StaticFileOptions { ServeUnknownFileTypes = true, DefaultContentType = "image/jpg" }; Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(builder => builder.Configure(app => app.UseStaticFiles(options))) .Build() .Run(); } }
上述解决方案只能设置一种默认媒体类型,如果具有多种需要映射成不同媒体类型的文件类型,采用这种方案就达不到目的,所以最根本的解决方案还是需要将不能识别的文件类型和对应的媒体类型进行映射。由于StaticFileMiddleware中间件使用的IContentTypeProvider对象是可以定制的,所以可以按照如下方式显式地为该中间件指定一个FileExtensionContentTypeProvider对象,然后将缺失的映射添加到这个对象上。
public class Program { public static void Main() { var contentTypeProvider = new FileExtensionContentTypeProvider(); contentTypeProvider.Mappings.Add(".img", "image/jpg"); var options = new StaticFileOptions { ContentTypeProvider = contentTypeProvider }; Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(builder => builder.Configure(app => app.UseStaticFiles(options))) .Build() .Run(); } }
静态文件中间件[1]: 搭建文件服务器
静态文件中间件[2]: 条件请求以提升性能
静态文件中间件[3]: 区间请求以提供部分内容
静态文件中间件[4]: StaticFileMiddleware
静态文件中间件[5]: DirectoryBrowserMiddleware & DefaultFilesMiddleware