深入理解 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 指标体系,将安全可观测性提升到了生产级水准。

相关推荐
亦良Cool20 分钟前
VMware虚拟机ubuntu瘦身,解决虚拟机越用越大
linux·运维·ubuntu
星辰&与海2 小时前
KVM + QEMU虚拟化方案
linux·运维
宋浮檀s2 小时前
应急响应——恶意流量&攻击行为识别
linux·运维·网络·网络安全·应急响应
REDcker2 小时前
Linux OverlayFS详解
java·linux·运维
zizle_lin3 小时前
WSL的系统安装和部分环境配置(按需操作)
运维
lwx9148523 小时前
Linux系统中用户锁定后如何解锁
linux·运维·服务器
難釋懷4 小时前
Nginx防盗链配置
运维·nginx
颖火虫盟主4 小时前
Linux 系统分层架构:从硬件通电到 systemd 进程管理
linux·运维·架构
cui_ruicheng4 小时前
Linux网络编程(九):应用层协议与序列化
linux·运维·服务器·网络
kobe_OKOK_4 小时前
ubuntu server 存儲空間占滿的原因
linux·运维·ubuntu