3. 认证方案与中间件

在 ASP.NET Core 中,认证不是一个“开关”,而是一条由方案(Scheme)、处理器(Handler)和中间件(Middleware)共同组成的流水线。很多初学者把 AddAuthentication、AddJwtBearer、UseAuthentication 三行代码当成固定咒语,却说不清它们各自在运行时扮演什么角色、谁先谁后、为什么顺序错了会拿到 401 而不是 403。本节把这条链路拆开讲清楚。

三个核心概念的分工

AuthenticationScheme(认证方案)是一个“名字 + 行为”的注册单元。它本身不做认证,而是把“我叫什么名字”和“由谁来处理我”绑定在一起。例如 Bearer、Cookies、Identity.Application 都是方案名。方案还携带一个 DisplayName,用于在 UI 或日志中展示。

AuthenticationHandler(认证处理器)是真正干活的类,继承自 AuthenticationHandler<TOptions>。它实现 HandleAuthenticateAsync(尝试从请求中解析凭据并构造 ClaimsPrincipal)、HandleChallengeAsync(未认证时如何响应,如返回 401 或跳转登录页)、HandleForbiddenAsync(已认证但无权限时如何响应,如返回 403)。JWT、Cookie、OAuth 各自有对应的内置 Handler。

UseAuthentication 中间件是执行入口。它不负责具体认证逻辑,而是在请求管道中调用 IAuthenticationService.AuthenticateAsync,把当前请求交给默认方案(或指定方案)的 Handler 去处理,并把结果写入 HttpContext.User。

注册:方案与处理器的绑定

注册发生在 Program.cs 的服务配置阶段。下面是一个同时启用 Cookie 与 JWT 的典型示例,注意方案名与默认方案的设置:

builder.Services.AddAuthentication(options =>
{
    // 默认方案:未显式指定时使用
    options.DefaultAuthenticateScheme = "Bearer";
    options.DefaultChallengeScheme = "Bearer";
})
.AddCookie("Cookies", options =>
{
    options.LoginPath = "/Account/Login";
    options.AccessDeniedPath = "/Account/Denied";
    options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
})
.AddJwtBearer("Bearer", options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidIssuer = "https://demo.example.com",
        ValidateAudience = true,
        ValidAudience = "demo-api",
        ValidateLifetime = true,
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes("demo-secret-key-at-least-32-bytes!"))
    };
});

这里 AddCookie("Cookies", ...) 和 AddJwtBearer("Bearer", ...) 各自完成两件事:注册一个方案名,并注册与之关联的 Handler 类型。方案名是后续 [Authorize(AuthenticationSchemes = "Bearer")] 引用的键。若省略方案名,会使用默认名(Cookie 为 Cookies,JWT 为 Bearer)。

默认方案(DefaultAuthenticateScheme / DefaultChallengeScheme)决定了“没有显式指定方案时用谁”。如果只注册了方案却没设默认值,UseAuthentication 在遇到需要认证的端点时可能抛出“No authenticationScheme was specified”异常。

执行顺序:中间件管道中的位置

注册只是“登记”,真正执行发生在请求管道。中间件顺序是 ASP.NET Core 中最容易出错的地方,认证相关顺序必须满足:UseRouting → UseAuthentication → UseAuthorization → MapControllers。

原因在于:UseRouting 先解析出当前请求匹配的端点(Endpoint),端点元数据里可能带有 [Authorize] 指定的方案;UseAuthentication 据此选择方案并调用 Handler 填充 HttpContext.User;UseAuthorization 再基于已填充的 User 和端点上的策略做授权判断。如果 UseAuthentication 放在 UseRouting 之前,端点尚未确定,认证就不知道该用哪个方案;如果放在 UseAuthorization 之后,授权时 User 还是空的,所有请求都会被判为未认证。

请求管道中的认证执行顺序 ✔ 正确顺序 UseRouting 解析端点 + 元数据 UseAuthentication 选方案 → 调 Handler UseAuthorization 基于 User + 策略判断 MapControllers 执行端点逻辑 AuthenticationHandler 内部三阶段 HandleAuthenticateAsync 解析凭据 → 构造 Principal HandleChallengeAsync 未认证 → 401 / 跳登录 HandleForbiddenAsync 已认证无权限 → 403 ✘ 常见错误顺序 UseAuthorization User 仍为空 UseAuthentication 认证结果来得太晚 所有请求被判未认证 返回 401,而非 403 结果判读 401 Unauthorized:认证失败或未提供凭据 → 检查 Handler 是否被调用、方案名是否匹配 403 Forbidden:认证成功但授权策略不通过 → 检查 Claims、角色、策略要求

运行时到底发生了什么

假设请求携带 Authorization: Bearer eyJhbGci... 访问一个标注了 [Authorize] 的接口,管道按以下顺序推进:

  1. UseRouting 匹配到该接口端点,端点元数据中包含 AuthorizeAttribute,其中可能指定了 AuthenticationSchemes = "Bearer"。
  2. UseAuthentication 调用认证服务。认证服务根据端点元数据或默认方案确定使用 Bearer 方案,找到注册时绑定的 JwtBearerHandler。
  3. JwtBearerHandler.HandleAuthenticateAsync 读取请求头,解析并校验 JWT 的签名、签发者、受众、有效期。校验通过则构造 ClaimsPrincipal,写入 HttpContext.User,并返回 AuthenticateResult.Success。
  4. UseAuthorization 读取 HttpContext.User,结合端点上的策略(如 RequireAuthenticatedUser 或自定义策略)判断是否放行。
  5. 若认证失败,授权中间件触发 ChallengeAsync,由 Handler 的 HandleChallengeAsync 返回 401;若认证成功但策略不满足,触发 ForbidAsync,返回 403。

多方案场景下的选择逻辑

当系统同时存在 Cookie 和 JWT 时,方案选择遵循“就近优先”原则:端点或控制器上显式指定的 AuthenticationSchemes 优先;未指定时使用 DefaultAuthenticateScheme;若默认方案也未设置,则尝试使用 DefaultScheme。这意味着同一个应用可以做到“网页端走 Cookie、API 端走 JWT”,只要在对应控制器上标注方案名即可:

[Authorize(AuthenticationSchemes = "Bearer")]
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    [HttpGet]
    public IActionResult Get() => Ok(new { user = User.Identity?.Name });
}
不要同时把 UseAuthentication 写在 UseRouting 之前又依赖端点元数据选择方案。此时端点尚未解析,认证只能依赖全局默认方案,容易在多方案应用中选错 Handler,表现为“明明带了 Token 却仍返回 401”。

排查顺序问题的实用检查表

现象优先检查判断依据
所有请求 401UseAuthentication 是否在 UseAuthorization 之前在授权中间件处打断点,观察 User 是否已填充
带了 Token 仍 401方案名是否与注册名一致对比 AddJwtBearer 的第一个参数与 Authorize 中的 Schemes
返回 403 而非 401认证已通过,问题在授权策略检查 Claims 是否包含策略要求的类型与值
启动即抛异常是否设置了默认方案异常信息通常为 “No authenticationScheme was specified”

把这三者的关系记成一句话:方案是名字,处理器是工人,中间件是调度员。注册阶段完成“名字—工人”的绑定,管道阶段由调度员按正确顺序把请求交给工人,工人产出的 ClaimsPrincipal 再交给授权环节消费。顺序一旦错位,整条链路就会在“未认证”状态上卡住,后续所有授权判断都失去意义。