ASP.NET Core后备端点元数据配置详解

本文深入探讨了ASP.NET Core中后备端点的元数据配置机制,详细分析了路由基础设施工作原理,并通过实例演示了如何为不同端点添加元数据来控制授权行为,特别指出了Razor Pages和MVC后备端点的特殊处理方式。

在ASP.NET Core中为后备端点添加元数据

在这篇文章中,我将描述ASP.NET Core中"后备"端点的元数据工作原理。首先简要讨论ASP.NET Core的路由基础设施,以及如何向端点添加元数据以驱动其他功能。接着描述什么是后备端点以及它们的用途。最后,说明为后备端点添加元数据的工作原理,以及为什么这在MVC和Razor Pages应用中可能不会如你最初预期的那样工作。

背景:ASP.NET Core中的路由基础设施

ASP.NET Core中的路由基础设施是中间件管道的基础组件,它是将传入URL映射到"处理程序"的主要方式,这些处理程序负责执行代码并生成响应。

例如,以下Hello World应用程序将单个路由/映射到一个处理程序(lambda方法),该方法返回字符串"Hello World!":

1
2
3
4
5
6
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

在这个简单示例中并不明显,但ASP.NET Core中的路由由两个主要中间件驱动:

  • EndpointRoutingMiddleware - 此中间件在运行时选择要为给定请求执行的已注册端点。它有时被称为RoutingMiddleware(这是我在本文中使用的名称)。
  • EndpointMiddleware - 此中间件通常放置在中间件管道的末端。该中间件执行RoutingMiddleware为给定请求选择的端点。

在.NET 6之前,你通常通过调用UseRouting()UseEndpoints()显式地将这些中间件添加到管道中。然而,WebApplication会自动为你添加这些,因此通常不需要显式调用。

向端点添加元数据以控制功能

如前所述,端点路由是ASP.NET Core的核心功能。向特定端点添加元数据对于控制不同端点的行为非常重要。有几个功能依赖元数据才能正常工作。

最常见的例子是AuthorizationMiddleware和CorsMiddleware,它们必须放置在RoutingMiddleware和EndpointMiddleware之间,以便它们知道要为所选端点应用哪些策略。

例如,你可能有一个适用于应用程序中所有端点的全局授权要求策略。然后,你将特定的"允许匿名访问"策略应用于"登录"和"忘记密码"端点,以便在你未登录时可以访问它们。

为此功能正常工作,你需要向登录和忘记密码策略应用元数据。你通常通过以下两种方式之一应用元数据:

  • 向MVC操作或Razor Page添加属性,例如[AllowAnonymous][Authorize]
  • 使用扩展方法,例如对最小API或其他端点使用AllowAnonymous()RequireAuthorization()

当RoutingMiddleware端点执行时,它会选择将要执行的端点。随后的中间件可以检查端点详细信息,查看是否有任何附加的中间件并相应行动。

让我们再次查看授权情况。例如,调用AllowAnonymous()方法会将AllowAnonymousAttribute的实例作为元数据添加到端点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static class AuthorizationEndpointConventionBuilderExtensions
{
    private static readonly IAllowAnonymous _allowAnonymousMetadata = new AllowAnonymousAttribute();

    public static TBuilder AllowAnonymous<TBuilder>(this TBuilder builder) where TBuilder : IEndpointConventionBuilder
    {
        builder.Add(endpointBuilder =>
        {
            endpointBuilder.Metadata.Add(_allowAnonymousMetadata);
        });
        return builder;
    }
}

类似地,调用RequireAuthorization()会添加AuthorizeAttribute的实例作为元数据:

1
2
3
4
5
6
7
public static class AuthorizationEndpointConventionBuilderExtensions
{
    public static TBuilder RequireAuthorization<TBuilder>(this TBuilder builder) where TBuilder : IEndpointConventionBuilder
    {
        return builder.RequireAuthorization(new AuthorizeAttribute());
    }
}

在RoutingMiddleware之后、EndpointMiddleware之前运行的AuthorizationMiddleware可以检查所选端点并读取此元数据,以决定对所选端点应用哪些授权策略。

在示例应用程序中尝试

我们可以在一个简单的最小API应用程序中尝试所有这些,只展示基础知识:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using Microsoft.AspNetCore.Authorization;

var builder = WebApplication.CreateBuilder(args);

// 配置基本cookie身份验证(以便授权工作)
builder.Services.AddAuthentication().AddCookie();
builder.Services.AddAuthorization(opt =>
{
    // 除非指定,否则必须登录才能获得授权
    opt.FallbackPolicy = new AuthorizationPolicyBuilder()
                            .RequireAuthenticatedUser()
                            .Build();
});

var app = builder.Build();

app.MapGet("/", () => "Hello World!"); // 除非登录,否则无法查看此页面
app.MapGet("/Account/Login", () => "Login page").AllowAnonymous(); // 始终可以查看此页面
app.Run();

这是一个简单的最小API,有两个端点:

  • / 主页
  • /Account/Login 登录页面

对于此应用程序,我添加了基本的身份验证和授权。我没有添加任何实际登录的方式,实际上我只是配置了最低要求。

然后我们配置一个全局策略,规定"除非另有指定,否则必须登录才能获得查看页面的授权"。结果是,如果我们尝试运行应用程序并导航到/,我们会自动重定向到/Account/Login页面,因为我们没有(也不能)登录。

但是,我们可以查看/Account/Login页面,因为它装饰有AllowAnonymous()元数据。

许多在中间件中实现但必须对特定端点表现不同的横切关注点功能都使用这种元数据方法。授权是典型示例,但CORS策略、OpenAPI文档或我的安全头库都以类似方式工作。

ASP.NET Core中的后备路由

路由是ASP.NET Core的核心组件,由许多不同的概念组成。例如:

  • 路由模式 - 这是应与传入请求匹配的URL路径模式
  • 处理程序 - 每个路由模式都有一个关联的处理程序,这是当模式匹配传入请求时运行以生成响应的代码
  • 路由参数 - 这些是路由模式中的可变部分,可以提取并自动转换为类型以供处理程序使用
  • 绑定 - 你可以自动从传入请求中提取详细信息以供处理程序使用

以及更多!

无论你使用ASP.NET Core的哪一部分,无论是MVC、Razor Pages、Blazor还是最小API,这些功能都会在不同程度上体现出来。

路由的基本第一步是决定传入URL匹配哪个路由模式。这是通过构建已注册端点的图,然后为传入URL找到正确匹配来完成的:

路由的每个路由都与一个处理程序相关联,但也有后备路由的概念。此路由匹配任何传入请求,只要该请求不被任何其他路由匹配,它就会调用提供的处理程序。

有许多不同的方法可以将后备路由添加到你的应用程序中,通常这将取决于你使用ASP.NET Core的哪一部分:最小API、MVC、Razor Pages等。

最简单的方法是调用MapFallback()方法,并提供一个要直接执行的处理程序。例如,我们可以向后备示例添加一个后备端点:

1
2
3
4
5
6
var app = builder.Build();

app.MapGet("/", () => "Hello World!");
app.MapGet("/Account/Login", () => "Login page").AllowAnonymous();
app.MapFallback(() => "Fallback"); // 👈 添加这个
app.Run();

现在,如果我们运行应用程序并点击任何随机URL,我们会重定向到/Account/Login页面。这是因为我们的后备"必须登录"授权策略生效,并将我们重定向。

在下图中,你可以看到/random-url被匹配,但自动重定向到我们的登录页面:

使用后备路由更常见的是将后备路由重定向到现有端点,无论是文件、MVC控制器还是Razor Page。

使用这样的后备路由的最常见原因是处理SPA应用程序。许多SPA应用程序在客户端处理"路由"并生成美观、正常外观的路由。但是,如果客户端刷新页面,那么"不正确"的路径将作为请求发送到ASP.NET Core。

例如,客户端SPA应用程序可能会向/something/customers/123发送请求,但这不一定对你的应用程序有意义。相反,在这种情况下,你通常需要返回你的"主页",让SPA应用程序运行其启动代码,然后在客户端进行路由。

确切地说"返回你的’主页’“意味着什么将取决于你的应用程序,但可能有一个MapFallback*重载适合你:

例如:

  • MapFallbackToFile(string filepath) 当没有路由匹配时返回文件,例如Index.html
  • MapFallbackToPath(string page) 执行给定的Razor Page作为后备
  • MapFallbackToController(string action, string controller) 执行指定的MVC控制器和操作作为后备

所有这些MapFallback()重载看起来相似,但在元数据方面实际上表现有些不同,我们将在下一节中看到。

简单端点的后备路由和元数据

通过思考我们一直在看的相同简单授权应用程序,我们可以探索元数据处理的差异。为了测试元数据的工作原理,我们可以简单地向MapFallback()方法添加AllowAnonymous()调用。

例如,采用我们最初的MapFallback()示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using Microsoft.AspNetCore.Authorization;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication().AddCookie();
builder.Services.AddAuthorization(opt =>
{
    opt.FallbackPolicy = new AuthorizationPolicyBuilder()
                            .RequireAuthenticatedUser()
                            .Build();
});

var app = builder.Build();

app.MapGet("/", () => "Hello World!"); 
app.MapGet("/Account/Login", () => "Login page").AllowAnonymous();
app.MapFallback(() => "Fallback").AllowAnonymous();
                                 // 👆 添加这个
app.Run();

在这种情况下,我所添加的只是对MapFallback()配置的AllowAnonymous()调用。在添加AllowAnonymous()调用之前,点击随机URL会导致未经授权的请求,我们会重定向到/Account/Login端点。但是,通过添加AllowAnonymous(),我们向后备端点添加了元数据,这意味着端点已获得授权,并为随机URL执行:

类似地,对于MapFallbackToFile()方法,添加AllowAnonymous()会将元数据附加到此后备端点。将上述MapFallback()调用更改为MapFallbackToFile("index.html")(并在wwwroot文件夹中向应用程序添加index.html文件)会得到相同的结果;点击任何未知URL都会返回index.html文件:

1
app.MapFallbackToFile("index.html").AllowAnonymous();

如果你认为Razor Page和基于MVC的后备方法表现类似,那是可以理解的,但有点令人惊讶的是,它们并不!

Razor Pages和MVC的后备路由和元数据

为了展示这一点,我创建了一个微小的Razor Pages应用程序,并添加了三个非常简单的Razor Pages,它们在一定程度上等同于上面的最小API版本:

/Index.chstml

1
2
@page
<h1>Index</h1>

/Account/Login.chstml - 注意这里的[AllowAnonymous]属性以允许匿名访问:

1
2
3
@page
@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]
<h1>Login</h1>

最后是/Fallback.cshtml

1
2
@page
<h1>Fallback</h1>

然后我向Razor pages应用程序添加了与之前相同的身份验证和授权服务,并使用MapFallbackToPage("/Fallback")添加了后备映射,并用AllowAnonymous()标记了该后备路由:

 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
using Microsoft.AspNetCore.Authorization;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

// 与之前相同的身份验证和授权服务
builder.Services.AddAuthentication().AddCookie();
builder.Services.AddAuthorization(opt =>
{
    opt.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
});


var app = builder.Build();

// 标准Razor Pages内容
app.UseRouting();
app.UseAuthorization();
app.MapStaticAssets();
app.MapRazorPages().WithStaticAssets();

// 添加后备页面并尝试将其标记为允许匿名
app.MapFallbackToPage("/Fallback").AllowAnonymous();
app.Run();

总体而言,这基本上等同于之前的最小API应用程序的Razor Pages版本。因此,如果我们导航到/Index,授权策略意味着我们被重定向到/Account/Login页面。该页面具有[AllowAnonymous]属性,因此我们可以查看该页面:

现在让我们尝试通过尝试随机URL来点击后备路由。看到我们在后备路由上标记了AllowAnonymous(),那么我们应该看到/Fallback页面,对吗?

嗯……这不对🤔似乎MapFallbackToPage()定义上的AllowAnonymous()调用不起作用?!

解释有点微妙……

为什么AllowAnonymous()在MapFallbackToPage()上不起作用?

MapFallback()MapFallbackToFile()调用实际上是在注册新端点;它们有一个捕获所有路由模式和一个处理程序,元数据与此新端点关联。

MapFallbackToPage()MapFallbackToController()的工作方式略有不同。这些确实添加了一个额外的端点,但该端点添加了额外的DynamicPageMetadata元数据(对于Razor Pages)或DynamicControllerMetadata元数据(对于MVC)。然后,Razor Pages/MVC基础设施找到此元数据并使用它来选择要执行的不同端点。

这是路由基础设施选择的端点,而不是"原始"后备端点。

这一切意味着"后备"端点基本上被它指向的真实页面替换。这也意味着你添加到该后备端点的任何元数据在实际调用时都会丢失,包括AllowAnonymous()调用!

换句话说,在MapFallbackToPage()MapFallbackToController()调用上调用AllowAnonymous()(或添加任何其他元数据)没有任何作用。

为了使我们的后备页面按我们希望的方式运行,我们必须将[AllowAnonymous]属性添加到目标页面,即/Fallback页面:

1
2
3
@page
@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]
<h1>Fallback</h1>

进行此更改后,现在如果我们点击随机URL,我们可以访问该页面,因为目标具有所需的元数据:

这基本上涵盖了一切。只需记住,如果你需要向后备Razor Page或MVC控制器添加元数据,那么你必须将其添加到目标端点,而不是"后备"端点本身。

总结

在本文中,我简要描述了ASP.NET Core的路由基础设施,以及如何向端点添加元数据以驱动其他功能。接着描述了什么是后备端点以及它们的用途。最后,我展示了在使用MapFallbackToPage()MapFallbackToController()创建后备端点时,添加元数据的工作方式不同。对于这些情况,后备端点被真实的目标端点替换。因此,如果你想向这些端点添加元数据,必须将其添加到目标端点而不是后备端点。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计