默认情况下,静态文件(如 HTML、CSS、图像和 JavaScript)是 ASP.NET Core 应用直接提供给客户端的资产。

提供静态文件

  静态文件存储在项目的 Web 根目录中。 默认目录为 {content root}/wwwroot,但可通过 UseWebRoot 方法更改目录。

  采用 CreateBuilder 方法可将内容根目录设置为当前目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

  可通过 Web 根目录的相关路径访问静态文件。 例如,Web 应用程序项目模板包含 wwwroot 文件夹中的多个文件夹:

  • wwwroot
    • css
    • js
    • lib

在 Web 根目录中提供文件

  默认 Web 应用模板在 Program.cs 中调用 UseStaticFiles 方法,这将允许提供静态文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

// 启用静态文件
app.UseStaticFiles();

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

  无参数 ·UseStaticFiles` 方法重载将 Web 根目录中的文件标记为可用。 以下标记引用 wwwroot/images/MyImage.jpg:

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

  波形符 ~ 指向 Web 根目录。

提供 Web 根目录外的文件

  考虑一个目录层次结构,其中要提供的静态文件位于 Web 根目录之外:

  • wwwroot
    • css
    • images
    • js
  • MyStaticFiles
    • images
    • red-rose.jpg

  按如下方式配置静态文件中间件后,请求可访问 red-rose.jpg 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using Microsoft.Extensions.FileProviders;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

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

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

  在前面的代码中,MyStaticFiles 目录层次结构通过 StaticFiles URI 段公开 。 对 https:///StaticFiles/images/red-rose.jpg 的请求将提供 red-rose.jpg 文件。

  以下标记引用 MyStaticFiles/images/red-rose.jpg:

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

设置HTTP响应标头

  StaticFileOptions 对象可用于设置 HTTP 响应标头。 除配置从 Web 根目录提供静态文件外,以下代码还设置 标头:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

var cacheMaxAgeOneWeek = (60 * 60 * 24 * 7).ToString();
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.Append(
"Cache-Control", $"public, max-age={cacheMaxAgeOneWeek}");
}
});

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

  上述代码使静态文件在本地缓存中公开可用一周(604800 秒)。

静态文件授权

  ASP.NET Core 模板在调用 UseAuthorization 之前调用 UseStaticFiles。 大多数应用都遵循此模式。 如果在授权中间件之前调用静态文件中间件:

  • 不会对静态文件执行任何授权检查。
  • 由静态文件中间件提供的静态文件(例如 wwwroot 下的文件)可公开访问。

  根据授权提供静态文件:

  • 将它们存储在 wwwroot 之外。
  • 调用 UseAuthorization 之后调用 UseStaticFiles,以指定路径。
  • 设置回退授权策略。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
using StaticFileAuth.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

// 添加授权认证策略
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
// 启用wwwroot目录静态文件(无需授权)
app.UseStaticFiles();

app.UseRouting();

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

// 启用StaticFiles目录静态文件(需要授权)
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles")),
RequestPath = "/StaticFiles"
});

app.MapRazorPages();

app.Run();

  在前面的代码中,回退授权策略要求所有用户进行身份验证。 用于指定其自己的授权要求的终结点(如控制器、Razor Pages 等)不使用回退授权策略。 例如,具有 [AllowAnonymous] 或 [Authorize(PolicyName=“MyPolicy”)] 的 Razor Pages、控制器或操作方法使用应用的授权属性,而不是回退授权策略。

  RequireAuthenticatedUserDenyAnonymousAuthorizationRequirement 添加到当前实例,这将强制对当前用户进行身份验证。

  wwwroot 下的静态资产是可公开访问的,因为在 UseAuthentication 之前会调用默认静态文件中间件 (app.UseStaticFiles();)。 MyStaticFiles 文件夹中的静态资产需要身份验证。

还有一种根据授权提供文件的方法是:

  • 将文件存储在 wwwroot 和静态文件中间件可访问的任何目录之外。
  • 通过应用授权的操作方法为其提供服务,并返回 FileResult 对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Authorize]
public class BannerImageModel : PageModel
{
private readonly IWebHostEnvironment _env;

public BannerImageModel(IWebHostEnvironment env) =>
_env = env;

public PhysicalFileResult OnGet()
{
var filePath = Path.Combine(
_env.ContentRootPath, "MyStaticFiles", "images", "red-rose.jpg");

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

  上述方法要求对每个文件使用一个页面或终结点。 以下代码为经过身份验证的用户返回文件或上传文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
app.MapGet("/files/{fileName}",  IResult (string fileName) => 
{
var filePath = GetOrCreateFilePath(fileName);

if (File.Exists(filePath))
{
return TypedResults.PhysicalFile(filePath, fileDownloadName: $"{fileName}");
}

return TypedResults.NotFound("No file found with the supplied file name");
})
.WithName("GetFileByName")
.RequireAuthorization("AuthenticatedUsers");

// IFormFile uses memory buffer for uploading. For handling large file use streaming instead.
// https://learn.microsoft.com/aspnet/core/mvc/models/file-uploads#upload-large-files-with-streaming
app.MapPost("/files", async (IFormFile file, LinkGenerator linker, HttpContext context) =>
{
// Don't rely on the file.FileName as it is only metadata that can be manipulated by the end-user
// Take a look at the `Utilities.IsFileValid` method that takes an IFormFile and validates its signature within the AllowedFileSignatures

var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
await SaveFileWithCustomFileName(file, fileSaveName);

context.Response.Headers.Append("Location", linker.GetPathByName(context, "GetFileByName", new { fileName = fileSaveName}));
return TypedResults.Ok("File Uploaded Successfully!");
})
.RequireAuthorization("AdminsOnly");

app.Run();

目录浏览

  目录浏览允许在指定目录中列出目录。出于安全考虑,目录浏览默认处于禁用状态。

  通过 AddDirectoryBrowserUseDirectoryBrowser 启用目录浏览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

// 注册目录浏览服务
builder.Services.AddDirectoryBrowser();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();


var fileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.WebRootPath, "images"));
var requestPath = "/MyImages";

// 启用静态文件
// Enable displaying browser links.
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = fileProvider,
RequestPath = requestPath
});

// 启用目录浏览
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
FileProvider = fileProvider,
RequestPath = requestPath
});

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

  上述代码允许使用 URL https:///MyImages 浏览 wwwroot/images 文件夹的目录,并链接到每个文件和文件夹:

  AddDirectoryBrowser 添加目录浏览中间件所需的服务,包括 HtmlEncoder。 这些服务可以通过其他调用(例如 AddRazorPages)添加,但我们建议调用 AddDirectoryBrowser 以确保将服务添加到所有应用中。

提供默认文档

  设置默认页面为访问者提供网站的起点。 若要从 wwwroot 提供默认文件,而不要求请求 URL 包含文件名,请调用 UseDefaultFiles 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

// 启用默认文档服务(必须在UseStaticFiles之前)
app.UseDefaultFiles();

app.UseStaticFiles();
app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

  要提供默认文件,必须在 UseStaticFiles 前调用 UseDefaultFilesUseDefaultFiles 是不提供文件的 URL 重写工具。

  使用 UseDefaultFiles 请求对 wwwroot 中的文件夹搜索:

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

  如同请求包含了文件名一样,提供从列表中找到的第一个文件。 浏览器 URL 继续反映请求的 URI。

  以下代码将默认文件名更改为 mydefault.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

// 使用mydefault.html作为默认文档
var options = new DefaultFilesOptions();
options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("mydefault.html");
app.UseDefaultFiles(options);

app.UseStaticFiles();

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

UseFileServer

  UseFileServer 结合了 UseStaticFilesUseDefaultFilesUseDirectoryBrowser(可选)的功能。

调用 app.UseFileServer 以提供静态文件和默认文件。 未启用目录浏览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

// 启用文件服务(包含静态文件服务、默认文档服务)
app.UseFileServer();

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

以下代码提供静态文件、默认文件和目录浏览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

// 提案加目录浏览服务
builder.Services.AddDirectoryBrowser();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

// 启用文件服务(包含静态文件服务、默认文档服务、目录浏览服务)
app.UseFileServer(enableDirectoryBrowsing: true);

app.UseRouting();

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

  考虑以下目录层次结构:

  • wwwroot
    • css
    • images
    • js
  • MyStaticFiles
    • images
    • MyImage.jpg
    • default.html

  以下代码提供静态文件、默认文件和 MyStaticFiles 的目录浏览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using Microsoft.Extensions.FileProviders;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

// 目录浏览
builder.Services.AddDirectoryBrowser();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

// StaticFiles目录(静态文件、目录浏览)
app.UseFileServer(new FileServerOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles")),
RequestPath = "/StaticFiles",
EnableDirectoryBrowsing = true
});

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

FileExtensionContentTypeProvider

  FileExtensionContentTypeProvider 类包含 Mappings 属性,用作文件扩展名到 MIME 内容类型的映射。 在以下示例中,多个文件扩展名映射到了已知的 MIME 类型。 替换了 .rtf 扩展名,删除了 .mp4 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

// 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
{
ContentTypeProvider = provider
});

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

非标准内容类型

  静态文件中间件可理解近 400 种已知文件内容类型。 如果用户请求文件类型未知的文件,则静态文件中间件将请求传递给管道中的下一个中间件。 如果没有中间件处理请求,则返回“404 未找到”响应。 如果启用了目录浏览,则在目录列表中会显示该文件的链接。

  以下代码提供未知类型,并以图像形式呈现未知文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

// 未知类型都已图片的形式呈现
app.UseStaticFiles(new StaticFileOptions
{
ServeUnknownFileTypes = true,
DefaultContentType = "image/png"
});

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

启用 ServeUnknownFileTypes 会形成安全隐患。 它默认处于禁用状态,不建议使用。 FileExtensionContentTypeProvider 提供了更安全的替代方法来提供含非标准扩展名的文件。

静态文件的安全注意事项

UseDirectoryBrowserUseStaticFiles`` 可能会泄漏机密。 强烈建议在生产中禁用目录浏览。 请仔细查看 UseStaticFilesUseDirectoryBrowser` 启用了哪些目录。 整个目录及其子目录均可公开访问。 将适合公开的文件存储在专用目录中,如 <content_root>/wwwroot。 将这些文件与 MVC 视图、Razor Pages 和配置文件等分开。

  • 使用 UseDirectoryBrowserUseStaticFiles 公开的内容的 URL 受大小写和基础文件系统字符限制的影响。 例如,Windows 不区分大小写,但 macOS 和 Linux 区分大小写。
  • 托管于 IIS 中的 ASP.NET Core 应用使用 ASP.NET Core 模块将所有请求转发到应用,包括静态文件请求。 不使用 IIS 静态文件处理程序,并且没有机会处理请求。
  • 在 IIS Manager 中完成以下步骤,删除服务器或网站级别的 IIS 静态文件处理程序
    1. 转到“模块”功能。
    2. 在列表中选择 StaticFileModule。
    3. 单击“操作”侧栏中的“删除” 。
  • 将代码文件(包括 .cs 和 .cshtml)放在应用项目的 Web 根目录之外。 这样就在应用的客户端内容和基于服务器的代码间创建了逻辑分隔。 可以防止服务器端代码泄漏。

如果启用了 IIS 静态文件处理程序且 ASP.NET Core 模块配置不正确,则会提供静态文件。 例如,如果未部署 web.config 文件,则会发生这种情况。

IWebHostEnvironment

  当 IWebHostEnvironment.WebRootPath 设置为 wwwroot 以外的文件夹时:

  • 在开发环境中,在 wwwroot 和更新的 IWebHostEnvironment.WebRootPath 中发现的静态资产由 wwwroot 提供。
  • 在开发环境以外的任何环境中,重复的静态资产会从更新的 IWebHostEnvironment.WebRootPath 文件夹提供。

  请考虑使用空 Web 模板创建的 Web 应用:

  • 在 wwwroot 和 wwwroot-custom 中包含一个 Index.html 文件。
  • 使用以下设置了 WebRootPath = "wwwroot-custom" 的已更新 Program.cs 文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in "wwwroot-custom"
WebRootPath = "wwwroot-custom"
});

var app = builder.Build();

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

app.Run();

  在前面的代码中,请求 /:

  • 在开发环境中,返回 wwwroot/Index.html
  • 在开发环境以外的任何环境中,返回 wwwroot-custom/Index.html
  • 若要确保返回 wwwroot-custom 中的资产,请使用以下方法之一:

  在 wwwroot 中删除重复的命名资产。

  • 将 Properties/launchSettings.json 中的 “ASPNETCORE_ENVIRONMENT” 设置为 “Development” 以外的任何值。
  • 通过在项目文件中设置 false,完全禁用静态 Web 资产。 警告,禁用静态 Web 资产会禁用 Razor 类库。
  • 将以下 JSON 添加到项目文件:
1
2
3
<ItemGroup>
<Content Remove="wwwroot\**" />
</ItemGroup>

  以下代码将 IWebHostEnvironment.WebRootPath 更新为非开发值,从而保证从 wwwroot-custom(而不是 wwwroot)返回重复内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Examine Hosting environment: logging value
EnvironmentName = Environments.Staging,
WebRootPath = "wwwroot-custom"
});

var app = builder.Build();

app.Logger.LogInformation("ASPNETCORE_ENVIRONMENT: {env}",
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"));

app.Logger.LogInformation("app.Environment.IsDevelopment(): {env}",
app.Environment.IsDevelopment().ToString());

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

app.Run();