一、认证与授权:从概念厘清开始
在进入技术细节之前,有必要把两个常被混淆的概念彻底分开:
认证(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:
3.1 Cookie 认证
本质:服务端生成加密 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 |
跳转登录页(用户已登录!) |
陷阱三:在 API 项目中遗留 Cookie 重定向逻辑
.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 指标体系,将安全可观测性提升到了生产级水准。