Public vs Private APIs in ASP.NET Core — 分支中间件管道(面向生产,带着微笑)
Source: Dev.to
请提供您希望翻译的正文内容(除代码块和 URL 之外的文本),我将把它翻译成简体中文并保持原有的格式、Markdown 语法和技术术语不变。谢谢!
目录
- 一句话概述问题
- 为何管道分支是正确的模式
- 按前缀使用 UseWhen(推荐)
- A1) 仅对 Public
/_api运行中间件 - A2) 仅对 Private
/api运行中间件 - A3) 使用扩展方法保持
Startup干净
- A1) 仅对 Public
- 选项 B – 端点元数据配置文件(企业灵活)
- 选项 C – 路由组(最小化 API)
- DI:“不注册” vs “不执行”
- 生产注意事项(OData + Controllers + 排序)
- 常见陷阱
- 最终建议
一句话概括的问题
您需要为访问 /api/... 与 /_api/... 的请求使用不同的中间件链,而 不 在每个中间件中混入路由逻辑。
为什么管道分支是正确的模式
| 好处 | 解释 |
|---|---|
| 关注点分离 | 中间件保持“纯粹”——内部没有特定路由的代码。 |
| 单一真相来源 | 所有基于路由的安全策略集中在一个位置(Startup 或扩展)。 |
| 性能 | 不匹配分支的请求永远不会实例化/执行那些中间件。 |
| 可演进性 | 易于从简单前缀扩展到以后更丰富的端点元数据。 |
按前缀使用 UseWhen(推荐)
当你的接口已经拥有明确的前缀(
/_api和/api)时,这是最简洁的解决方案。
A1) 仅为 Public /_api 运行中间件
// ------------------------------------------------------------
// 1️⃣ Place after UseRouting() and before endpoint mapping
// ------------------------------------------------------------
app.UseRouting();
// ✅ Branch: only /_api
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments(
"/_api", StringComparison.OrdinalIgnoreCase),
branch =>
{
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
});
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
A2) 仅为 Private /api 运行中间件
app.UseRouting();
// ✅ Branch: only /api
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments(
"/api", StringComparison.OrdinalIgnoreCase),
branch =>
{
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
});
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
A3) 使用扩展方法保持 Startup 整洁
// ──────────────────────────────────────────────────────────────
// Extension methods that encapsulate the branching logic
// ──────────────────────────────────────────────────────────────
public static class ApiSecurityPipelineExtensions
{
public static IApplicationBuilder UsePublicApiSecurity(this IApplicationBuilder app)
{
return app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments(
"/_api", StringComparison.OrdinalIgnoreCase),
branch =>
{
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
});
}
public static IApplicationBuilder UsePrivateApiSecurity(this IApplicationBuilder app)
{
return app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments(
"/api", StringComparison.OrdinalIgnoreCase),
branch =>
{
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
});
}
}
在 Configure 中的使用方式
app.UseRouting();
app.UsePublicApiSecurity(); // 或 app.UsePrivateApiSecurity();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
选项 B — 端点元数据配置文件(企业灵活版)
当你需要 例外(例如,某些 /_api 端点应跳过堆栈,或某些 /api 端点应选择加入)时,将决定从 路径 移到 元数据。
1️⃣ 定义自定义属性
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple = false)]
public sealed class SecurityMiddlewareProfileAttribute : Attribute
{
public SecurityMiddlewareProfileAttribute(string profile) => Profile = profile;
public string Profile { get; }
}
2️⃣ 集中管理配置文件常量
public static class SecurityProfiles
{
public const string Public = "public";
public const string Private = "private";
public const string None = "none"; // 跳过所有安全中间件
}
3️⃣ 为控制器 / 操作标注
[SecurityMiddlewareProfile(SecurityProfiles.Public)]
[Route("_api/report/v1/[controller]")]
public class ReportController : ControllerBase
{
// …
}
4️⃣ 基于端点元数据进行分支(路由之后)
app.UseRouting();
app.UseWhen(ctx =>
{
var endpoint = ctx.GetEndpoint();
var profile = endpoint?.Metadata
.GetMetadata<SecurityMiddlewareProfileAttribute>()
?.Profile;
// 仅在 “public” 配置文件时执行堆栈(根据需要调整)
return string.Equals(profile, SecurityProfiles.Public,
StringComparison.OrdinalIgnoreCase);
},
branch =>
{
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
});
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
现在,你可以在不修改中间件代码的情况下,对每个端点的行为进行精细调控。
选项 C — 路由组(如果你转向 Minimal API)
var publicGroup = app.MapGroup("/_api")
.AddEndpointFilter(async (context, next) =>
{
// optional per‑group filter logic
return await next(context);
});
publicGroup.UseMiddleware();
publicGroup.UseMiddleware();
publicGroup.UseMiddleware();
publicGroup.UseMiddleware();
publicGroup.UseMiddleware();
publicGroup.MapGet("/report/v1/{id}", (int id) => /* … */);
Minimal‑API 路由组为你提供相同的分支能力,同时拥有更流畅的语法。
DI:“不注册” 与 “不执行”
| 方法 | 何时使用 |
|---|---|
不在 ConfigureServices 中注册 中间件 | 当中间件 构造成本高,且在特定部署中永远不需要时。 |
| 不执行(分支) | 当中间件构造成本低,但只应在部分请求中运行时。 |
在大多数情况下,分支(
UseWhen)已经足够,因为中间件管道在启动时只构建一次;分支仅会跳过不匹配请求的执行。
Production Notes (OData + Controllers + Ordering)
- 将
UseWhen放在UseRouting之后 – 在路由之前端点元数据(包括 OData 路由)不可用。 - 在
UseAuthentication/UseAuthorization之前 如果这些中间件必须在分支之外运行(例如,你仍希望在私有端点上进行身份验证)。- 如果你只想在公共分支上进行身份验证,请将身份验证调用 移到 分支内部。
- OData 路由注册 (
app.UseEndpoints(endpoints => endpoints.MapODataRoute(...))) 应该放在 分支之后,这样分支才能看到最终的端点。 - 安全中间件的顺序 很重要 – 在每个分支内部保持与线性管道相同的顺序。
常见陷阱
| 陷阱 | 症状 | 解决方案 |
|---|---|---|
| 分支放置在 UseRouting 之前 | ctx.GetEndpoint() 为 null;前缀检查可能仍然有效,但后续基于路由的元数据将失效。 | 将分支移动到 app.UseRouting() 之后。 |
使用 StartsWith 而不是 StartsWithSegments | /api2/... 错误地匹配了 /api。 | 使用 StartsWithSegments 进行基于段的匹配。 |
在需要时忘记在分支内部调用 app.UseAuthentication() | 分支内部的请求未经过身份验证。 | 在分支内部添加身份验证中间件,或如果应适用于所有请求则全局保留。 |
| 注册了相同的中间件 两次(一次全局,一次在分支中) | 工作重复,可能产生副作用。 | 仅在需要的地方注册。 |
在 URL 重写后(例如 UsePathBase)依赖 HttpContext.Request.Path | 路径可能已被修改,导致不匹配。 | 使用 ctx.Request.PathBase + Path,或相应调整谓词。 |
最终建议
- 首先使用基于前缀的
UseWhen方法——它最简单、性能最佳,并且能够保持中间件的纯粹性。 - 将分支逻辑封装在扩展方法中,以保持
Startup.Configure的整洁。 - 当需要对单个端点进行例外处理时,切换到 端点元数据配置文件(选项 B)。
- 如果采用 Minimal API,考虑使用 路由组(选项 C)以获得更流畅的使用体验。
通过对管道进行分支,你可以实现关注点的清晰分离,避免不必要的中间件执行,并在 API 范围扩展时保持安全模型的灵活性。
Source: …
在 ASP.NET Core 中按端点控制中间件
以下是原始讨论的精简版,保持相同的结构和内容。
使用 UseWhen 进行分支
app.UseWhen(context => context.Request.Path.StartsWithSegments("/_api"), branch =>
{
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
branch.UseMiddleware();
});
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
✅ 结果: 你可以以声明式方式为每个端点控制中间件行为。
Minimal‑API 替代方案 – 路由组
如果以后迁移到 Minimal API,路由组可以提供“完美”的语法:
var publicApi = app.MapGroup("/_api");
publicApi.UseMiddleware();
publicApi.UseMiddleware();
var privateApi = app.MapGroup("/api");
// privateApi.UseMiddleware();
注意: 控制器不以同样的一等公民方式支持路由组,所以目前
UseWhen仍是最安全的做法。
“不执行” vs. “不注册”
你说:“当是私有的时,我既不想执行它们,也不想在依赖中注册它们。”
在 ASP.NET Core 中,你无需注销中间件来阻止其执行:
| 场景 | 行为 |
|---|---|
位于 UseWhen(...) 内且路由 不匹配 | • 中间件 不会执行。 • 通常 不会解析(DI 不会创建实例)。 |
| 位于任何条件分支之外 | 中间件会对每个请求运行。 |
因此真正的目标是 “不执行”;“不注册”往往会增加不必要的复杂度。
如果你仍希望在某些部署中避免注册,可将其视为 环境特定的配置(例如,仅在生产环境注册某些中间件)。
顺序很重要
UseRouting() 必须在检查路由元数据的任何分支之前运行。
| 放置位置 | 效果 |
|---|---|
在 UseRouting() 之后(甚至之前) – 前缀检查 | 简洁且适用于大多数场景。 |
在 UseRouting() 之后 – 端点元数据检查 | 必须,因为元数据在此之前不可用。 |
在端点映射之后(MapControllers()、MapHub() 等) | 该分支 不会影响 请求。 |
在 UseRouting() 之前 – 元数据检查 | GetEndpoint() 将为 null。 |
典型的管道布局
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting(); // ("/callshub");
常见陷阱
- 把
UseWhen放得太晚(在端点映射之后) → 永远不会运行。 - 在
UseRouting()之前检查端点元数据 →GetEndpoint()为null。 - 混合前缀检查和元数据检查且没有明确规则 → 可能导致中间件运行两次。
- 将中间件逻辑耦合到 “公开/私有” → 保持中间件 纯粹,让管道决定 何时 运行它们。
推荐方案
-
先使用方案 A – 基于前缀的
UseWhen。
它匹配你当前的 API 表面(/_api与/api),并能快速上线。 -
当需要更细粒度的端点控制时,演进为 方案 B – 元数据配置文件(例如自定义端点元数据属性)。
-
保持中间件可组合且纯粹;将 “何时运行” 的逻辑放在管道配置中。
后续步骤
如果你贴出控制器的路由方式(包括任何 OData 路由),我可以生成精确的 UseWhen 块以及最安全的排序,以确保不会与 OData 路由或 UseEndpoints() 冲突。
祝发布顺利! 🚀