深入理解 ASP.NET Core Authentication Scheme 体系

一、认证与授权:从概念厘清开始

在进入技术细节之前,有必要把两个常被混淆的概念彻底分开:

认证(Authentication) :确定请求方是谁------"你是谁?"。凭据无效则返回 401 Unauthorized。

授权(Authorization) :确定已认证用户能做什么------"你能干什么?"。权限不足则返回 403 Forbidden。

在 ASP.NET Core 中,认证由 IAuthenticationService 负责,该服务通过已注册的认证处理器(Handler)完成具体认证动作,产出 ClaimsPrincipal,授权管道再据此做权限决策。两者是严格先后执行的两个中间件,不可颠倒,不可混用。


二、Architecture:三层组件模型

整个认证体系由三个层次构成:

复制代码
┌───────────────────────────────────────────────────────┐
│             Authentication Middleware                  │
│           UseAuthentication()  调用                    │
│              IAuthenticationService                    │
└──────────────────────┬────────────────────────────────┘
                       │ 按 Scheme Name 路由
       ┌───────────────┼───────────────────┐
       ▼               ▼                   ▼
 JwtBearerHandler  CookieHandler    CustomHandler
       │               │                   │
       └───────────────┴───────────────────┘
                       │
                  AuthenticateResult
                       │
              HttpContext.User = ClaimsPrincipal
                       │
                       ▼
           UseAuthorization Middleware
            (AddPolicy 策略评估)

Middleware 层 :UseAuthentication() 拦截每一个请求,触发认证流程,将结果写入 HttpContext.User。必须在所有依赖认证状态的中间件(包括 UseAuthorization)之前调用。

IAuthenticationService 层:认证服务的核心接口,对外暴露五个操作:

方法 触发时机 说明
AuthenticateAsync 每次请求 解析请求凭据,构建 ClaimsPrincipal
ChallengeAsync 未认证访问受保护资源 重定向登录页或返回 401
ForbidAsync 已认证但无权限 返回 403
SignInAsync 登录成功 签发凭证(如写 Cookie)
SignOutAsync 登出 销毁凭证

Handler 层 :HandleAuthenticateAsync() 方法从 Request 构建 ClaimsPrincipal,成功则封装成 AuthenticationTicket 返回 AuthenticateResult.Success(ticket),失败则返回 AuthenticateResult.Fail(...)。


三、Scheme 体系全景

Authentication Scheme 是一个具名配置,将处理器(Handler)与其配置选项绑定在一起,是认证系统的核心抽象。ASP.NET Core 内置了五大类 Scheme:

本质:服务端生成加密 Cookie,客户端持 Cookie 访问。有状态,支持服务端主动吊销。

适用场景:传统 MVC Web 应用、需要服务端注销(踢下线)、需要滑动过期的 Session 管理。

csharp 复制代码
.AddCookie("Cookies", options =>
{
    options.LoginPath = "/Account/Login";
    options.AccessDeniedPath = "/Account/Denied";
    options.ExpireTimeSpan = TimeSpan.FromHours(8);
    options.SlidingExpiration = true;        // 活跃时自动续期
    options.Cookie.HttpOnly = true;          // 防 XSS
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Strict; // 防 CSRF
})

.NET 10 重要变化 :在此之前,Cookie Handler 对 API 端点上的未认证请求会返回 302 重定向而非 401,开发者必须在 OnRedirectToLogin 事件中手动处理。.NET 10 已移除这一特殊逻辑,API 端点现在直接返回正确的 401,无需 workaround。

3.2 JWT Bearer 认证

本质:客户端携带自包含的 JWT Token,服务端验签。无状态,天然支持水平扩展。

适用场景:前后端分离 API、移动端、微服务间调用、多平台客户端。

csharp 复制代码
.AddJwtBearer("Bearer", options =>
{
    options.Authority = "https://login.microsoftonline.com/{tenantId}/v2.0";
    options.Audience = "api://my-api";
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ClockSkew = TimeSpan.FromSeconds(30),
    };
    // 支持 SignalR / gRPC 的 Query String 传 Token
    options.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            var token = context.Request.Query["access_token"];
            if (!string.IsNullOrEmpty(token) &&
                context.HttpContext.Request.Path.StartsWithSegments("/hub"))
                context.Token = token;
            return Task.CompletedTask;
        }
    };
})

3.3 OpenID Connect(OIDC)认证

本质:OAuth2 之上的身份层,支持 SSO(Single Sign-On),可获取 ID Token(包含用户身份声明)。

适用场景:企业 SSO(Azure AD、Okta、Keycloak)、社会化登录的标准实现、需要 ID Token 的场景。

csharp 复制代码
.AddOpenIdConnect("oidc", options =>
{
    options.Authority = "https://login.microsoftonline.com/{tenantId}/v2.0";
    options.ClientId = "web-app-client-id";
    options.ClientSecret = "...";
    options.ResponseType = OpenIdConnectResponseType.Code; // PKCE
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");
    options.SaveTokens = true;                      // Token 存入 Cookie
    options.GetClaimsFromUserInfoEndpoint = true;
    options.CallbackPath = "/signin-oidc";
})

3.4 OAuth 认证

本质:纯授权层,获取第三方资源访问令牌,不直接提供用户身份,需自行调用 UserInfo API 组装用户信息。

适用场景:GitHub/Twitter/Google 第三方登录、获取第三方 API 资源(如读取用户 GitHub 仓库)。

csharp 复制代码
.AddOAuth("GitHub", options =>
{
    options.ClientId = "github-client-id";
    options.ClientSecret = "github-secret";
    options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
    options.TokenEndpoint      = "https://github.com/login/oauth/access_token";
    options.UserInformationEndpoint = "https://api.github.com/user";
    options.Scope.Add("user:email");
    options.ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
    options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
    options.Events = new OAuthEvents
    {
        OnCreatingTicket = async context =>
        {
            var request = new HttpRequestMessage(
                HttpMethod.Get, context.Options.UserInformationEndpoint);
            request.Headers.Authorization =
                new AuthenticationHeaderValue("Bearer", context.AccessToken);
            var response = await context.Backchannel.SendAsync(request);
            var user = await JsonDocument.ParseAsync(
                await response.Content.ReadAsStreamAsync());
            context.RunClaimActions(user.RootElement);
        }
    };
})

OIDC vs OAuth :如果第三方 IdP 支持 OIDC,优先用 AddOpenIdConnect,它会自动处理 UserInfo 请求和 Claims 映射;AddOAuth 适用于仅支持 OAuth2 但未实现 OIDC 的平台(如早期的 GitHub)。

3.5 Negotiate(Windows 认证)

本质:NTLM/Kerberos 协商认证,域内用户无感知自动登录。

适用场景:企业内网应用、Active Directory 域环境、内网管理工具。

csharp 复制代码
.AddNegotiate(options =>
{
    // Linux 服务器需要配置 LDAP(Windows 服务器自动集成)
    options.EnableLdap("ldap.contoso.com");
})

3.6 自定义 Handler

当内置 Handler 无法满足需求时(如 API Key、HMAC 签名认证),继承 AuthenticationHandler<TOptions> 实现:

csharp 复制代码
public class ApiKeyAuthHandler : AuthenticationHandler<ApiKeyAuthOptions>
{
    private readonly IApiKeyValidator _validator;

    public ApiKeyAuthHandler(
        IOptionsMonitor<ApiKeyAuthOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        IApiKeyValidator validator)
        : base(options, logger, encoder)
    {
        _validator = validator;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.TryGetValue(Options.HeaderName, out var apiKey))
            return AuthenticateResult.NoResult();  // 没有凭据,非此方案管辖

        var principal = await _validator.ValidateAsync(apiKey.ToString());
        if (principal is null)
            return AuthenticateResult.Fail("Invalid API Key");

        var ticket = new AuthenticationTicket(principal, Scheme.Name);
        return AuthenticateResult.Success(ticket);
    }

    protected override Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        // API 接口不做重定向,直接返回 401
        Response.StatusCode = 401;
        Response.Headers.WWWAuthenticate =
            $"ApiKey realm=\"{Options.HeaderName}\"";
        return Task.CompletedTask;
    }
}

3.7 Scheme 选型速查

应用类型 推荐 Scheme 组合
传统 MVC Web 应用 Cookie
SPA + REST API JWT Bearer
企业 Web 应用(SSO) OIDC + Cookie
第三方社会化登录 OAuth + Cookie
企业内网工具(域内) Negotiate
Web 应用 + API 双栈 Cookie + JWT Bearer
多 IdP 共存 PolicyScheme + 多 JWT
无密码现代应用 Passkey + Cookie(.NET 10)
微服务内部服务间调用 JWT Bearer + 自定义 API Key

四、Default Scheme 体系:五种默认方案

当注册了多个 Scheme 时,需要明确告知框架在不同操作下应使用哪个 Scheme。ASP.NET Core 提供了五种粒度的默认方案配置:

配置项 作用 未配置时的行为
DefaultScheme 所有操作的兜底方案 若只注册一个 Scheme,自动成为默认
DefaultAuthenticateScheme AuthenticateAsync 调用时使用 回退到 DefaultScheme
DefaultChallengeScheme ChallengeAsync 调用时使用 回退到 DefaultScheme
DefaultSignInScheme SignInAsync 调用时使用 回退到 DefaultScheme
DefaultForbidScheme ForbidAsync 调用时使用 回退到 DefaultScheme

典型配置------OIDC 登录 + Cookie 持久化(企业 SSO 标配):

csharp 复制代码
builder.Services.AddAuthentication(options =>
{
    // 日常请求:走轻量 Cookie 验证
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    // 需要登录时:触发 OIDC 外部跳转
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("oidc", options => { /* ... */ });

这种分离设计的精妙之处在于:只有第一次登录时才触发 OIDC 的外部跳转(重型流程),后续所有请求均通过本地 Cookie 验证(轻量流程),既保留了 SSO 能力,又避免了每次都往返外部 IdP 的性能损耗。

单 Scheme 自动推断 :当只注册了一个 Scheme 时,它自动成为 DefaultScheme,无需显式指定。如需禁用此行为:

csharp 复制代码
AppContext.SetSwitch(
    "Microsoft.AspNetCore.Authentication.SuppressAutoDefaultScheme", true);

五、Policy Scheme:多方案动态路由

当同一应用需要同时支持多种 IdP 或认证机制时,Policy Scheme 提供了一个"路由层",将外部统一 Scheme 名称在内部动态分发到具体 Handler。

5.1 转发优先级

Policy Scheme 的转发决策按以下优先级从高到低执行:

复制代码
ForwardAuthenticate / ForwardChallenge / ForwardForbid  ← 操作级,最具体
                   ForwardDefaultSelector               ← 请求级动态函数
                      ForwardDefault                    ← 全局静态兜底

AuthenticationSchemeOptions 中各转发属性的语义:

csharp 复制代码
public class AuthenticationSchemeOptions
{
    // 静态:将所有操作转发到指定 Scheme
    public string ForwardDefault { get; set; }

    // 静态:将特定操作转发到指定 Scheme(最高优先级)
    public string ForwardAuthenticate { get; set; }
    public string ForwardChallenge { get; set; }
    public string ForwardForbid { get; set; }
    public string ForwardSignIn { get; set; }
    public string ForwardSignOut { get; set; }

    // 动态:根据当前 HttpContext 返回目标 Scheme 名称
    public Func<HttpContext, string> ForwardDefaultSelector { get; set; }
}

5.2 静态路由

在配置阶段确定转发目标,不依赖请求内容,适合 Scheme 分工明确的场景:

csharp 复制代码
// 场景:所有 Challenge 走 OIDC(触发 SSO),其余操作走 Cookie
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = "Combined";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options => { /* ... */ })
.AddPolicyScheme("Combined", displayName: null, options =>
{
    options.ForwardDefault    = "Cookies";  // 认证/登出等走 Cookie
    options.ForwardChallenge  = "oidc";     // 仅 Challenge 走 OIDC
});

5.3 动态路由

通过 ForwardDefaultSelector 在运行时检查请求内容,适合多 IdP、混合认证等复杂场景:

csharp 复制代码
// 场景:同一 API 网关同时服务 Azure AD 用户、Auth0 用户和内部服务 API Key
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = "Smart";
    options.DefaultChallengeScheme = "Smart";
})
.AddJwtBearer("AzureAD", options =>
{
    options.Authority = "https://login.microsoftonline.com/{tenantId}/v2.0";
    options.Audience = "api://my-api";
})
.AddJwtBearer("Auth0", options =>
{
    options.Authority = "https://my-tenant.auth0.com/";
    options.Audience = "https://api.contoso.com";
})
.AddScheme<ApiKeyAuthOptions, ApiKeyAuthHandler>("ApiKey", options =>
{
    options.HeaderName = "X-Api-Key";
})
.AddPolicyScheme("Smart", displayName: null, options =>
{
    options.ForwardDefaultSelector = context =>
    {
        // 内部服务携带 API Key
        if (context.Request.Headers.ContainsKey("X-Api-Key"))
            return "ApiKey";

        // 解析 JWT Issuer 决定路由
        var bearer = context.Request.Headers.Authorization.ToString();
        if (bearer.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
        {
            var token = bearer["Bearer ".Length..].Trim();
            var handler = new JsonWebTokenHandler();
            if (handler.CanReadToken(token))
            {
                var issuer = handler.ReadJsonWebToken(token).Issuer;
                if (issuer.Contains("microsoftonline.com")) return "AzureAD";
                if (issuer.Contains("auth0.com"))           return "Auth0";
            }
        }

        return "AzureAD"; // 兜底
    };
});

动态路由执行流程:

复制代码
HTTP Request
     │
     ▼
PolicyScheme ("Smart")
     │
     │  ForwardDefaultSelector(context) 执行
     │  ↓ 检查 Header
     │
     ├── X-Api-Key ────────────────────→ ApiKeyAuthHandler
     │
     ├── Bearer (issuer=microsoftonline) → JwtBearerHandler("AzureAD")
     │
     └── Bearer (issuer=auth0)    ──────→ JwtBearerHandler("Auth0")

六、AddPolicy vs AddPolicyScheme:彻底厘清

这是开发者最常混淆的地方,两者名字相似,但所处的层次完全不同。

6.1 本质区别

维度 AddPolicy(授权策略) AddPolicyScheme(策略方案)
所属层 授权(Authorization)层 认证(Authentication)层
注册位置 AddAuthorization(o => o.AddPolicy(...)) AddAuthentication(...).AddPolicyScheme(...)
解决什么问题 定义"谁能访问某资源"的规则 定义"如何选择认证 Handler"
执行时机 认证之后,路由匹配时 认证阶段,Handler 选择时
输入 已认证的 ClaimsPrincipal HTTP Request(Header、路径等原始请求数据)
输出 authorized / failed 决策 目标 Handler 的 Scheme 名称(字符串)

一句话总结:AddPolicyScheme 决定"用哪把锁开门",AddPolicy 决定"持什么证件才能进门"。

6.2 完整执行流程

复制代码
HTTP Request
     │
[认证阶段] UseAuthentication
     │
     │  1. 查找 DefaultAuthenticateScheme(或端点指定的 Scheme)
     │  2. 若是 PolicyScheme → 执行 ForwardDefaultSelector
     │     → 返回目标 Scheme 名称
     │  3. 路由到具体 Handler(JWT / Cookie / 自定义)
     │  4. Handler.HandleAuthenticateAsync()
     │  5. 成功 → HttpContext.User = ClaimsPrincipal
     │
[授权阶段] UseAuthorization
     │
     │  6. 读取端点上的 [Authorize(Policy="X")]
     │  7. 从 AddPolicy 字典中查找策略 X 的规则
     │  8. 评估 ClaimsPrincipal 是否满足规则
     │
     ├── 满足 → 执行 Action / Endpoint
     └── 不满足 → ForbidAsync → 403

6.3 AddPolicy 的五种规则类型

csharp 复制代码
builder.Services.AddAuthorization(options =>
{
    // 1. 角色断言
    options.AddPolicy("AdminOnly", policy =>
        policy.RequireRole("Admin", "SuperAdmin"));

    // 2. Claim 断言(最常用)
    options.AddPolicy("PremiumUser", policy =>
        policy.RequireClaim("subscription", "premium", "enterprise"));

    // 3. 限定认证 Scheme(只接受特定 Scheme 的身份)
    options.AddPolicy("JwtOnly", policy =>
        policy.AddAuthenticationSchemes("AzureAD", "Auth0")
              .RequireAuthenticatedUser());

    // 4. 自定义需求(IAuthorizationRequirement)
    options.AddPolicy("MinTenure2Years", policy =>
        policy.Requirements.Add(new MinTenureRequirement(years: 2)));

    // 5. 兜底策略(所有端点默认要求认证,除非标记 [AllowAnonymous])
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

七、Scheme 组合使用的四种模式

模式一:Web + API 双轨制

前端页面走 Cookie,API 接口走 JWT,互不干扰:

csharp 复制代码
builder.Services.AddAuthentication()
    .AddCookie("Cookies", options => { /* ... */ })
    .AddJwtBearer("Bearer", options => { /* ... */ });

// MVC Controller:Cookie 认证
[Authorize(AuthenticationSchemes = "Cookies")]
public IActionResult Dashboard() => View();

// API Controller:JWT 认证
[Authorize(AuthenticationSchemes = "Bearer")]
[Route("api/[controller]")]
public class DataController : ControllerBase { }

// Minimal API 写法
app.MapGet("/api/data", () => Results.Ok())
   .RequireAuthorization(new AuthorizeAttribute
   {
       AuthenticationSchemes = "Bearer"
   });

模式二:OIDC + Cookie(企业 SSO 标配)

只有第一次登录触发 OIDC 跳转,后续请求走本地 Cookie:

csharp 复制代码
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options =>
{
    options.ExpireTimeSpan = TimeSpan.FromHours(8);
    options.SlidingExpiration = true;
})
.AddOpenIdConnect("oidc", options =>
{
    options.Authority = "https://login.microsoftonline.com/{tenantId}/v2.0";
    options.ClientId = "web-app";
    options.SaveTokens = true;
    // 刷新 Token(避免 Cookie 过期后重新登录)
    options.UseTokenLifetime = false;
});

模式三:多 IdP + Policy Scheme(微服务 API 网关)

见上文"动态路由"部分,此处不再重复。

模式四:Passkey + Cookie(.NET 10 无密码登录)

csharp 复制代码
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<AppDbContext>()
    .AddPasskeys();  // .NET 10 新增

builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
    options.ServerDomain = "contoso.com";
    options.AuthenticatorTimeout = TimeSpan.FromMinutes(5);
    options.ChallengeSize = 32;
});

// Passkey 认证成功后,Identity 自动调用 SignInManager 写入 Cookie
// 后续请求走 Cookie 认证,无需每次重走 WebAuthn 流程

八、Passkey:无密码认证的现代实现

8.1 核心概念

Passkey 是基于 WebAuthn 和 FIDO2 标准的密码替代方案。私钥保存在用户设备(Windows Hello、Touch ID、Face ID 或 YubiKey),公钥存储在服务器。认证时用户用私钥对服务端挑战(challenge)进行签名,服务端用公钥验证,私钥永不离开设备。

三大核心安全优势:

  • 抗钓鱼:Passkey 与特定域名绑定,伪造站点无法使用
  • 无共享密钥:服务器只存公钥,数据库泄露也不会暴露私钥
  • 用户便利:生物识别替代记忆密码

三个关键角色:**Relying Party(RP)**即 ASP.NET Core 服务器;Authenticator 即用户设备;WebAuthn API 即浏览器中的 JavaScript 桥梁。

8.2 注册流程(Attestation)

复制代码
用户(Browser)                         ASP.NET Core Server
    │                                            │
    │──── POST /passkeys/creation-options ───────→│
    │                                            │  生成唯一 challenge
    │←── PublicKeyCredentialCreationOptions ──────│  (challenge + rpId + userId)
    │                                            │
    │  navigator.credentials.create() 被调用
    │  设备弹出 Windows Hello / Touch ID / Face ID
    │  用户完成生物验证
    │  Authenticator 为该域名生成专属公私钥对
    │                                            │
    │──── POST /passkeys/register ───────────────→│
    │   (credentialId + 公钥 + attestation 数据)  │  1. 验证 challenge 签名
    │                                            │  2. 存储公钥 + credentialId
    │                                            │  3. 关联用户账号
    │←──────────── 注册成功 ─────────────────────│

8.3 认证流程(Assertion)

复制代码
用户(Browser)                         ASP.NET Core Server
    │                                            │
    │──── POST /passkeys/assertion-options ──────→│
    │                                            │  生成新 challenge
    │←── PublicKeyCredentialRequestOptions ───────│  (challenge + allowCredentials)
    │                                            │
    │  navigator.credentials.get() 被调用
    │  浏览器展示可用 Passkey 列表
    │  用户完成生物验证
    │  Authenticator 用私钥对 challenge 签名
    │                                            │
    │──── POST /passkeys/login ──────────────────→│
    │   (credentialId + 签名数据 + clientData)    │  1. 用存储的公钥验证签名
    │                                            │  2. 检查 challenge 一致性
    │                                            │  3. 签名有效 → 调用 SignInManager
    │←──────────── Cookie 写入,认证成功 ─────────│

8.4 .NET 10 支持的场景与限制

支持的场景:

  • 向已有密码账号追加 Passkey(密码 + Passkey 双模式)
  • 无密码建账(注册时直接创建 Passkey,不设密码)
  • 无密码登录(仅凭 Passkey 完成认证)

当前限制:

  • API 仅适用于 ASP.NET Core Identity,非通用 WebAuthn 库
  • 默认不验证 attestation 声明(需第三方库补充)
  • Blazor Web App 模板内置,其他模板需手动接入
  • Passkey 作为主要认证因素,不是第二因素

安全注意事项:

生产环境中必须显式配置 ServerDomain,否则实现会从 Host Header 推断 RP ID,存在凭据作用域攻击风险。所有 Passkey 操作都要求 HTTPS,并应配置 HSTS 防止协议降级。


九、.NET 10 认证可观测性指标体系

9.1 .NET 10 之前的盲区

在 .NET 10 之前,ASP.NET Core 内置 Meter 只覆盖了 HTTP 层(Hosting、Kestrel)和路由层。HTTP 请求进入业务逻辑后------认证是否成功、授权如何决策------完全是盲区,需开发者自行埋点。

9.2 .NET 10 新增三个 Meter

.NET 10 填补了这个空白,新增三个遵循 OpenTelemetry 语义规范的 Meter,形成完整的可观测链路:

复制代码
HTTP 请求到达          路由匹配            认证执行              授权决策
(Hosting meter) → (Routing meter) → (Authentication meter) → (Authorization meter)
                                          ↓
                                    (Identity meter)
                               身份操作(登录/注册/2FA 等)

Authentication Meter(Microsoft.AspNetCore.Authentication):

指标名 类型 说明
aspnetcore.authentication.authenticate.duration Histogram 认证操作耗时(附 Scheme 维度)
aspnetcore.authentication.challenges Counter Challenge 次数(未认证访问受保护资源)
aspnetcore.authentication.forbids Counter Forbid 次数(已认证但无权限)
aspnetcore.authentication.sign_ins Counter 登录次数
aspnetcore.authentication.sign_outs Counter 登出次数

Authorization Meter(Microsoft.AspNetCore.Authorization):

指标名 类型 说明
aspnetcore.authorization.attempts Counter 授权尝试次数,附 result=authorized/failed 维度

Identity Meter(Microsoft.AspNetCore.Identity):

指标名 说明
aspnetcore.identity.sign_in.sign_ins 登录成功/失败次数
aspnetcore.identity.sign_in.check_password_attempts 密码验证次数(监控暴力破解)
aspnetcore.identity.sign_in.authenticate.duration 登录整体耗时
aspnetcore.identity.user.generated_tokens Token 生成次数

9.3 接入 OpenTelemetry

csharp 复制代码
// 安装包:
// OpenTelemetry.Extensions.Hosting
// OpenTelemetry.Instrumentation.AspNetCore
// OpenTelemetry.Exporter.Prometheus.AspNetCore

builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()           // HTTP 层(.NET 8 起)
        .AddMeter("Microsoft.AspNetCore.Authentication")  // .NET 10 新增
        .AddMeter("Microsoft.AspNetCore.Authorization")   // .NET 10 新增
        .AddMeter("Microsoft.AspNetCore.Identity")        // .NET 10 新增
        .AddPrometheusExporter());

// 暴露 /metrics 端点(生产环境建议加 IP 白名单或 JWT 保护)
app.MapPrometheusScrapingEndpoint()
   .RequireAuthorization("InternalMonitoring");

9.4 本地快速验证

bash 复制代码
dotnet tool install -g dotnet-counters

dotnet-counters monitor --process-id <pid> \
  --counters Microsoft.AspNetCore.Authentication,\
             Microsoft.AspNetCore.Authorization,\
             Microsoft.AspNetCore.Identity

9.5 生产告警设计

yaml 复制代码
groups:
  - name: auth_security_alerts
    rules:
      # 密码验证失败激增 → 可能是撞库/暴力破解攻击
      - alert: HighPasswordFailureRate
        expr: |
          rate(aspnetcore_identity_sign_in_check_password_attempts_total
            {result="failed"}[5m]) > 10
        for: 2m
        annotations:
          summary: "密码验证失败率过高,疑似暴力破解"

      # Challenge 激增 → 大量未认证请求扫描受保护接口
      - alert: SurgeInAuthChallenges
        expr: rate(aspnetcore_authentication_challenges_total[1m]) > 50
        for: 1m
        annotations:
          summary: "认证挑战激增,疑似接口扫描行为"

      # 授权拒绝率超过 30% → 权限配置问题或越权尝试
      - alert: AuthorizationFailureSpike
        expr: |
          rate(aspnetcore_authorization_attempts_total{result="failed"}[5m])
          /
          rate(aspnetcore_authorization_attempts_total[5m]) > 0.3
        for: 3m
        annotations:
          summary: "授权失败率超过 30%,请检查权限配置"

      # 认证耗时 P99 过高 → 可能是 IdP 连接问题或 Token 验证瓶颈
      - alert: SlowAuthentication
        expr: |
          histogram_quantile(0.99,
            rate(aspnetcore_authentication_authenticate_duration_seconds_bucket[5m])
          ) > 1
        annotations:
          summary: "认证 P99 耗时超过 1 秒,检查 IdP 连接"

十、完整架构流程图(.NET 10)

X-Api-Key Header
Bearer

issuer=microsoftonline
Bearer

issuer=auth0
Cookie
域内请求
WebAuthn

.NET 10
Success
NoResult / Fail
是
否
通过
拒绝
HTTP Request
UseAuthentication

IAuthenticationService
PolicyScheme

动态路由层

ForwardDefaultSelector
ApiKeyAuthHandler

自定义
JwtBearerHandler

AzureAD
JwtBearerHandler

Auth0
CookieAuthHandler
NegotiateHandler

Windows Auth
Passkey

  • Identity Cookie
    AuthenticateResult
    HttpContext.User

= ClaimsPrincipal
需要认证?
ChallengeAsync

401 / OIDC 跳转
匿名访问通过
UseAuthorization

AddPolicy 策略评估
执行 Endpoint

Controller / Minimal API
ForbidAsync

403 Forbidden
OpenTelemetry Metrics

Microsoft.AspNetCore.Authentication

Microsoft.AspNetCore.Authorization

Microsoft.AspNetCore.Identity
Prometheus → Grafana

告警 / 仪表盘


十一、最佳实践与常见陷阱

陷阱一:多方案下未指定默认方案

注册了多个 Scheme 但未指定默认值,在 [Authorize] 不指明方案名的情况下会抛出 InvalidOperationException。解决方式:要么在 AddAuthentication(defaultScheme) 中指定默认值,要么在每个 [Authorize] 上显式注明方案。

陷阱二:Challenge 与 Forbid 混用

场景 正确行为 错误做法
用户未登录访问受保护页面 ChallengeAsync → 跳转登录页 直接返回 403
已登录但权限不足 ForbidAsync → 403 跳转登录页(用户已登录!)

.NET 10 之前必须手动处理 OnRedirectToLogin 事件,.NET 10 已内置修复,旧的 workaround 代码应当删除,否则可能引发双重处理。

陷阱四:Passkey 的 ServerDomain 安全配置

不显式配置 ServerDomain 会从 Host Header 推断 RP ID,存在被攻击的风险。生产环境必须显式设置,且该域名下的所有子域都不能托管不受信内容。

最佳实践清单

  • 给每个 Scheme 取具有业务含义的名称(如 "AzureAD" 而非 "Bearer1")
  • Web 页面与 API 端点使用不同 Scheme,避免状态码混乱
  • 对敏感端点通过 AddPolicy 显式指定所需 Scheme,不依赖全局默认
  • 接入 OpenTelemetry 三层 Meter,在 Grafana 中建立认证安全仪表盘
  • 生产环境的 /metrics 端点加 IP 白名单或 JWT 保护
  • Passkey 生产部署前务必启用 HTTPS + HSTS,并显式配置 ServerDomain
  • PolicyScheme 的 ForwardDefaultSelector 函数应轻量无副作用,避免阻塞认证管道

结语

ASP.NET Core 的认证体系以 Scheme 为核心抽象,分三层构建:Middleware 负责拦截请求、IAuthenticationService 按名称路由到 Handler、Handler 实现具体凭据验证逻辑。Policy Scheme 在此之上增加了动态路由能力,优雅解决多 IdP 并存问题。AddPolicyScheme 属于认证层,决定"用哪个锁";AddPolicy 属于授权层,决定"持什么证件"------理解这一根本区别,是掌握整个体系的关键。

.NET 10 在此基础上带来了三项重量级升级:修正了 Cookie Scheme 在 API 端点上长期存在的重定向问题;通过 WebAuthn/FIDO2 标准原生支持无密码登录(Passkey);以及覆盖认证、授权、身份操作三层的 OpenTelemetry 指标体系,将安全可观测性提升到了生产级水准。

相关推荐
QYR-分析1 小时前
刚需赛道:全球一次性有创压力传感器市场深度解析
云计算
judezh10 小时前
api 容器一直 unhealthy?我把一个 Agent 运行时的健康检查逐条拆了,查出四个问题
运维·docker
AKAMAI10 小时前
随着瓦尔·基尔默的AI分身诞生,生成式AI是否正在将好莱坞推向边缘?
人工智能·云原生·云计算
春天花会开13110 小时前
内网 HTTPS 部署实战:私有 CA + Nginx 反向代理全流程复盘
运维·nginx·https
AllFiles10 小时前
K8s Node Exporter 异常 Write 超时排查实录
运维·后端·kubernetes
虎头金猫6 天前
4K 视频总卡在公网带宽?用 N1 + OpenList 把网盘播放链路重新理顺
运维·服务器·网络·python·容器·beautifulsoup·pandas
AI职业加油站6 天前
AI智能体应用工程师证书:政策红利下的职业新风口
大数据·运维·人工智能·学习·职场发展
此冬歌咏6 天前
K8s 节点故障实战:优雅驱逐 31 秒,硬故障 331 秒,以及那个永远 Pending 的 Pod
运维·k8s
kybs19916 天前
全球灾害数据分析可视化 毕业设计-附源码66794
vue.js·spring boot·mysql·安全·django·c#·asp.net
-梅6 天前
linux(8) 软硬链接
linux·运维·服务器