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:贯穿认证与授权的数据载体。
这些组件的关系可以用下面的全景图表示。图中左侧是请求进入方向,中间是认证与授权两个阶段,右侧是端点执行与结果状态。
三、从“能跑”到“可工程化”
很多项目最初的安全实现是“能登录、能拦住未登录用户”就结束了。但工程化要求更高:认证方案要可替换,授权规则要可组合,失败语义要可区分,配置要可测试。
以 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 断点、策略定义 |