1. 认证授权工程全景

在 ASP.NET Core 中,“安全”并不是一个单独的中间件或一个开关,而是一条贯穿请求生命周期的处理链。理解这条链上每个环节的职责边界,是后续所有认证、授权、令牌、策略、权限设计工作的前提。本节先建立一张全景图:请求从哪里进入、经过哪些安全组件、认证与授权各自负责什么、工程化目标又是什么。

一、认证与授权的边界

认证(Authentication)回答的是“你是谁”,授权(Authorization)回答的是“你能做什么”。这两件事在概念上必须严格分开,但在实现上又紧密衔接。

认证的产物是一个 ClaimsPrincipal,它由若干 ClaimsIdentity 组成,每个身份携带一组声明(Claim),例如 sub、name、role、scope。认证只负责把请求中携带的凭据(Cookie、Bearer Token、证书等)转换成这个主体对象,并设置 HttpContext.User。它不判断这个用户能不能访问某个接口。

授权发生在认证之后,它读取 HttpContext.User 以及请求的端点元数据(Endpoint Metadata),判断当前主体是否满足策略要求。如果认证失败,HttpContext.User 是一个未认证的匿名主体,授权通常会直接拒绝。因此,认证是授权的前置条件,但认证成功不等于授权通过。

一个常见误区:把“登录成功”等同于“有权限访问”。登录只解决身份问题,接口能否访问由授权策略决定。例如一个已登录的普通用户访问 /api/admin/users,认证通过但授权应返回 403 而不是 401。

二、核心组件与请求处理链

ASP.NET Core 的安全能力由一组可组合的组件构成,它们按顺序参与请求处理:

  • Authentication Middleware:调用已注册的认证处理器(Handler),尝试从请求中解析凭据并构建 ClaimsPrincipal。
  • Authentication Handler:具体认证方案的实现,如 JwtBearerHandler、CookieAuthenticationHandler。它负责解析、校验、构造身份。
  • Authorization Middleware:在端点执行前触发授权评估,读取端点上的 [Authorize] 或策略元数据。
  • Authorization Policy / Requirement / Handler:策略是要求的集合,要求由处理器评估,处理器返回成功或失败。
  • IAuthorizationService:授权评估的统一入口,支持在中间件之外(如服务层、Blazor 组件)主动调用。
  • ClaimsPrincipal / ClaimsIdentity / Claim:贯穿认证与授权的数据载体。

这些组件的关系可以用下面的全景图表示。图中左侧是请求进入方向,中间是认证与授权两个阶段,右侧是端点执行与结果状态。

ASP.NET Core 请求安全处理链 HTTP 请求 Cookie / Bearer / 证书 认证阶段 Authentication Authentication Middleware 按方案调用 Handler Authentication Handler 解析凭据 · 校验签名/有效期 ClaimsPrincipal 写入 HttpContext.User 授权阶段 Authorization Authorization Middleware 读取端点元数据 Policy / Requirement 策略与要求集合 AuthorizationHandler 评估声明/角色/自定义规则 端点执行 Controller / Minimal API 业务逻辑 返回 200 / 403 认证失败 → 401 未认证 / 凭据无效 授权失败 → 403 已认证但策略不满足 工程化目标 1. 认证方案可插拔:Cookie / JWT / OIDC / 证书按场景组合,不把凭据解析写进业务代码。 2. 授权策略集中声明:策略名、要求、处理器注册在启动期完成,端点只引用策略名。 3. 失败语义清晰:401 表示身份未确认,403 表示身份已确认但权限不足,前端据此分流。

三、从“能跑”到“可工程化”

很多项目最初的安全实现是“能登录、能拦住未登录用户”就结束了。但工程化要求更高:认证方案要可替换,授权规则要可组合,失败语义要可区分,配置要可测试。

以 JWT Bearer 为例,一个可工程化的最小配置如下。注意 TokenValidationParameters 中的每一项都对应一个明确的校验目标,而不是“填上就行”:

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "https://auth.example.com",
            ValidateAudience = true,
            ValidAudience = "order-api",
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromSeconds(30),
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(configuration["Jwt:SigningKey"]!))
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanReadOrders", policy =>
        policy.RequireAuthenticatedUser()
              .RequireClaim("scope", "orders.read"));
});

这段配置体现了三个工程化要点:第一,认证方案在注册期确定,业务代码不感知 Token 如何解析;第二,授权策略以命名方式集中声明,端点只需写 [Authorize(Policy = "CanReadOrders")];第三,ClockSkew 被显式设置为 30 秒,而不是使用默认的 5 分钟,避免令牌过期后仍被接受的窗口过长。

四、判断结果:401 与 403 的分工

在排障时,状态码是最直接的线索。401 Unauthorized 表示认证阶段没有产出有效身份,通常原因包括:未携带 Token、Token 签名不匹配、Issuer/Audience 不匹配、Token 已过期。403 Forbidden 表示认证已通过,但授权处理器返回失败,通常原因包括:缺少所需 Claim、角色不匹配、策略中的自定义要求未满足。

状态码发生阶段典型原因排查入口
401认证Token 缺失、签名错误、过期、Issuer/Audience 不符AuthenticationHandler 日志、TokenValidationParameters
403授权缺少 Claim、角色不符、自定义 Requirement 失败AuthorizationHandler 断点、策略定义
不要把 403 当成 401 处理。前端若把 403 也跳转到登录页,会导致已登录用户被反复要求登录,掩盖真正的权限配置问题。

五、本节小结与后续衔接