NET CORE 认证模块-注册方案与请求认证探究

📅 2026/7/22 7:10:42 👁️ 阅读次数 📝 编程学习
NET CORE 认证模块-注册方案与请求认证探究

认证模块的注册
这一步的本质,是注册认证系统赖以生存的核心基础设施,它不涉及任何具体的认证逻辑(比如怎么读 Cookie、怎么解 JWT),而是定义认证系统应该怎么运转。

前面介绍过 Cookie 和 JWT 认证是怎么在代码里集成的,它们有个共同点:在启动类集成时都会加上这么一句。其实不光这两个,任何认证方案都离不开下面这段代码。

var authenticationBuilder = builder.Services.AddAuthentication(options =>
{
});
看名字就知道它是「添加身份认证」的意思,返回一个 AuthenticationBuilder。那它到底添加了什么?它是一个容器的扩展方法,我们直接进它内部看实现。

2.1 基础设施层

先看 AuthenticationServiceCollectionExtensions:

public static AuthenticationBuilder AddAuthentication(this IServiceCollection services)
{
// 添加认证核心组件
services.AddAuthenticationCore();
// 添加数据保护组件
services.AddDataProtection();
// 添加 Web 编码组件
services.AddWebEncoders();
// 添加时间处理组件
services.TryAddSingleton<ISystemClock, SystemClock>();
// services.TryAddSingleton(TimeProvider.System); // .NET 8 改用这个
// 添加认证配置的集中管理组件
services.TryAddSingleton<IAuthenticationConfigurationProvider, DefaultAuthenticationConfigurationProvider>();
return new AuthenticationBuilder(services);
}
这里就是认证模块最核心的几个组件注册,它们是 .NET Core 认证平台提供的基础,也是模块能跑起来的关键。逐个说一下:

AddDataProtection:注册数据保护服务(IDataProtectionProvider),为认证模块提供加密能力。主要用来加密 Cookie 票据、保护 Token 签名密钥,以及防止敏感数据被篡改。
IAuthenticationConfigurationProvider:负责从 IConfiguration 里加载认证相关的配置,比如 JWT 的签发者(Issuer)、受众(Audience)、密钥来源这些。有了它,认证方案就能从 appsettings.json 或环境变量里读配置,不用硬编码。
AddAuthenticationCore:注册认证模块的核心服务,包括执行认证、获取认证处理器、管理认证方案,是整个认证功能的骨架。
ISystemClock:提供统一的 UTC 时间源,在校验 Cookie、JWT 是否过期时调用。好处是单元测试时可以注入 Mock 来模拟时间变化。
AddWebEncoders:负责认证模块里一些 URL 的编解码工作,比如处理回调 URL、编码 Cookie 数据、编解码令牌相关数据。
可以这么理解:加密保护、配置读取、核心服务、时间校验、数据编码这几块凑在一起,共同构成了认证模块这套基础设施。

image

2.2 核心引擎 AddAuthenticationCore

如果说 AddDataProtection 和 ISystemClock 是保证引擎正常运转的润滑油和电源,那么 AddAuthenticationCore 就是这台引擎的缸体和曲轴——它定义了动力产生的核心逻辑。

所以这部分我们主要围绕 AddAuthenticationCore 展开。用红花绿叶来打比方,AddAuthenticationCore 就是那朵红花,其他几个是衬托它的绿叶,但绿叶也缺一不可。理解成「引擎」更贴切,后面顺带也会简单聊下 IAuthenticationConfigurationProvider。

看 AuthenticationCoreServiceCollectionExtensions:

public static IServiceCollection AddAuthenticationCore(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

services.TryAddScoped<IAuthenticationService, AuthenticationService>(); services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>(); // Can be replaced with scoped ones that use DbContext services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>(); services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>(); return services;

}
AddAuthenticationCore() 里注册了 4 个关键服务。这段代码展示的是引擎内部的构造,它不涉及任何具体的 Cookie 或 JWT,只是单纯定义了「必须具备哪些功能」。当它执行完毕,.NET Core 的认证舞台就具备了运转能力。

image

(1)认证入口 IAuthenticationService

这是认证系统统一对外的接口,默认实现是 AuthenticationService,定义了 5 个核心方法:

public interface IAuthenticationService
{
// 1. 认证:获取请求的任何认证数据(如解析 Cookie/Token 得到用户身份)
Task AuthenticateAsync(HttpContext context, string scheme);

// 2. 质询:用于未经身份验证的请求(如返回 401 状态码,或重定向到登录页) Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties); // 3. 禁止:当已认证用户尝试访问不允许访问的资源时使用(如返回 403 状态码) Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties); // 4. 登录:建立关联,将用户主体(ClaimsPrincipal)写入认证票据 Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties); // 5. 登出:移除任何相关联的认证数据(如清除 Cookie) Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties);

}
参考上一篇的 Cookie 登录,当我们在控制器里调用 HttpContext.SignInAsync() 时,实际上就是在调用这个服务。不过它自己不干活,只负责根据指令找到方案,然后交给方案里具体的 Handler 去执行。

你会发现这 5 个方法都要接收一个 scheme 参数。为什么都要一个 scheme? 因为可能同时存在多个方案(比如 Cookie + JWT),得明确告诉它这次操作针对的是哪个方案。

(2)全局配置数据容器 AuthenticationOptions

AuthenticationOptions 是认证模块的全局配置容器,内部维护了一个 SchemeMap 字典,用来存储所有已注册方案的名称和对应的 AuthenticationSchemeBuilder。比如我们调用 AddCookie(),最终就是往 SchemeMap 里加入 Cookie 方案。

它主要存三部分信息:

默认方案:如 DefaultAuthenticateScheme、DefaultChallengeScheme、DefaultSignInScheme、DefaultSignOutScheme、DefaultForbidScheme。
方案列表:所有已注册方案的名称和显示名称。
方案与 Handler 的映射:每个方案名称对应一个 AuthenticationSchemeBuilder,其中包含了 HandlerType。
它本身只是数据的容器,具体的注册逻辑由 AddScheme 操作它,具体的查询逻辑由 IAuthenticationSchemeProvider 读取它。

(3)方案注册与字典管理 IAuthenticationSchemeProvider

IAuthenticationSchemeProvider 负责管理所有注册过的认证方案(Scheme),默认实现是 AuthenticationSchemeProvider。当调用 AddCookie 或 AddJwtBearer 注册方案时,这些方案信息(比如名字叫 Cookies、对应的 Handler 类型是什么)都会被存放到类似这样的字典里:

Dictionary<string, 方案> _schemes = new ();
_schemes.Add(“cookie”, “Cookie方案”);
_schemes.Add(“jwt”, “Jwt方案”);
这里有一条清晰的链路:

AddCookie() 把方案写入 AuthenticationOptions.SchemeMap(数据仓库)→ IAuthenticationSchemeProvider 从仓库里查询 → 查到的结果交给 IAuthenticationHandlerProvider 去实例化。

(4)处理器的实例化激活 IAuthenticationHandlerProvider

IAuthenticationHandlerProvider 负责实例化具体的认证处理器(Handler),默认实现是 AuthenticationHandlerProvider。

上一步 IAuthenticationSchemeProvider 只提供了具体的方案,IAuthenticationHandlerProvider 则是根据方案,把真正的执行对象从 DI 容器里拉出来,放到当前的请求上下文中。

(5)身份二次加工 IClaimsTransformation

IClaimsTransformation 用于在认证成功后,对用户的身份声明(Claims)进行二次加工或转换。默认注入的是 NoopClaimsTransformation,也就是什么都不做——这代表用户认证通过后,拿到的 Claims 原封不动。但如果你想在用户登录后,自动从数据库查一下他的最新角色并追加进身份里,就可以自己实现这个接口,替换掉默认实现。

3.认证方案的注册
基础设施搭好之后,AddScheme 的作用就是把具体的认证策略(Handler)和配置(Options)绑起来,挂到刚搭好的「方案管理器」里。

上面认证的基本运转能力已经具备了,接下来我们挑 JWT 作为探究对象。

3.1 调用扩展方法,进入注册流程

先看 JwtBearerExtensions:

// 添加 JWT 方案
public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string? displayName, Action configureOptions)
{
// 用于从配置文件中自动绑定基础选项
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions, JwtBearerConfigureOptions>());

// 在所有常规配置执行完毕后,提供默认兜底配置 builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>()); // 将 JWT Bearer 认证方案与对应的处理器(JwtBearerHandler)绑定 return builder.AddScheme<JwtBearerOptions, JwtBearerHandler>(authenticationScheme, displayName, configureOptions);

}
AddJwtBearer() 是一个静态扩展方法,内部先注册了「配置绑定」和「兜底配置」两个服务,然后调用 AuthenticationBuilder.AddScheme<JwtBearerOptions, JwtBearerHandler>(),把具体的选项类型 JwtBearerOptions 和处理器类型 JwtBearerHandler 作为泛型参数传进去,正式进入通用的方案注册流程。

3.2 进入 AddSchemeHelper

看 AuthenticationBuilder:

private AuthenticationBuilder AddSchemeHelper<TOptions, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>(
string authenticationScheme,
string? displayName,
Action? configureOptions)
where TOptions : AuthenticationSchemeOptions, new()
where THandler : class, IAuthenticationHandler
{
var state = new AddSchemeHelperState(typeof(THandler));

// 1. 配置全局的 AuthenticationOptions,将当前方案名称、处理器类型、显示名称注册进去 Services.Configure<AuthenticationOptions>(o => { o.AddScheme(authenticationScheme, scheme => { scheme.HandlerType = state.HandlerType; scheme.DisplayName = displayName; }); }); // 2. 如果有自定义的配置委托,则将其绑定到当前方案名称上 if (configureOptions != null) { Services.Configure(authenticationScheme, configureOptions); } // 3. 为当前方案的选项添加验证逻辑,构建时通过内置的 Validate 方法检查 Services.AddOptions<TOptions>(authenticationScheme).Validate(o => { o.Validate(authenticationScheme); return true; }); // 4. 将自定义的认证处理器 Handler 注册为瞬态服务 Services.AddTransient<THandler>(); return this;

}
它才是这部分的主角,作用就是「添加一种方案」,而添加需要几个对象:

方案需要的配置 TOptions:有约束,它必须是 AuthenticationSchemeOptions 的派生类。
方案的具体执行者 THandler:也有要求,它必须是 AuthenticationHandler 类型的派生类。
这里说「派生类」而不是「子类」,是因为中间隔着几层继承也是允许的。

接着看,AddScheme 最终调用了私有方法 AddSchemeHelper。它的第一个动作是实例化一个 AddSchemeHelperState 对象,把 THandler(也就是 JwtBearerHandler)的类型信息存进它的 HandlerType 属性。这个对象的作用,是在后面的委托链里保留 Handler 的类型信息——因为 Configure 委托是延后执行的,得先把类型捕获下来。

3.3 AuthenticationOptions 注册方案映射

AddSchemeHelper 通过下面这段:

Services.Configure(o =>
{
o.AddScheme(authenticationScheme, scheme =>
{
scheme.HandlerType = state.HandlerType;
scheme.DisplayName = displayName;
});
});
把当前方案信息写入全局配置对象 AuthenticationOptions。看 AuthenticationOptions:

public class AuthenticationOptions
{
private readonly IList _schemes = new List();

public IEnumerable<AuthenticationSchemeBuilder> Schemes => _schemes; // 定义了一个方案字典 public IDictionary<string, AuthenticationSchemeBuilder> SchemeMap { get; } = new Dictionary<string, AuthenticationSchemeBuilder>(StringComparer.Ordinal); public void AddScheme(string name, Action<AuthenticationSchemeBuilder> configureBuilder) { if (name == null) { throw new ArgumentNullException(nameof(name)); } if (configureBuilder == null) { throw new ArgumentNullException(nameof(configureBuilder)); } if (SchemeMap.ContainsKey(name)) { throw new InvalidOperationException("Scheme already exists: " + name); } var builder = new AuthenticationSchemeBuilder(name); configureBuilder(builder); _schemes.Add(builder); SchemeMap[name] = builder; }

}
AuthenticationOptions.AddScheme 做了这么几件事:

校验方案名称是否已存在,防止重复注册。
实例化一个 AuthenticationSchemeBuilder,传入方案名称(例如 Bearer),再把 AddSchemeHelperState 里保存的 HandlerType(即 JwtBearerHandler 的类型)赋值给 Builder 的 HandlerType 属性。
image

把 Builder 同时加到 _schemes 列表和 SchemeMap 字典中,以方案名称为键。
如果调用 AddJwtBearer 时传了 configureOptions 委托,那么此时也会把用于配置 Issuer、Audience 的委托绑定到对应的方案名称上,让后面解析 JwtBearerOptions 时能按方案名称拿到对应的配置实例。
最后再调用 Services.AddTransient(),把具体的认证处理器 JwtBearerHandler 注册到 DI 容器,每次请求需要时重新实例化。

这一步走完,AuthenticationOptions 内部维护的 SchemeMap 字典里,就包含了方案名称和 AuthenticationSchemeBuilder 的映射关系。

3.4 方案注册小结

先看整体流程图:

image

整个注册流程走完后,依赖注入容器里就有了:

AuthenticationOptions 的 SchemeMap 字典,存储了所有注册方案的名称、显示名称和对应的 HandlerType。
每个方案对应的 Options 配置实例,按方案名称隔离存好。
每个方案对应的 Handler 类型,注册为瞬态服务由容器统一管理。
用大白话概括:一通操作下来,其实就干了一件重要的事——把方案转成一个 AuthenticationSchemeBuilder 对象,然后存进 AuthenticationOptions 里,等着后续使用。

4.请求认证执行
UseAuthentication 负责把注册好的认证系统真正接入 HTTP 请求的处理管道,它本质上就是注册了 AuthenticationMiddleware 中间件。

打个比方:如果把 HTTP 请求比作水管里的水,那 UseAuthentication 就是给水管中间又加了一截,这一截专门负责对水做检查——放到这里,就是对 HTTP 请求做检查,看你有没有经过认证。那接下来要看的,就是 HTTP(水)在这一截水管里,到底做了哪些事。

源码:< https://source.dot.net/#Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs ,20a6e8d8983fbe5c>

public class AuthenticationMiddleware
{
private readonly RequestDelegate _next;

public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes) { // 校验参数,确保中间件管道和认证方案提供者不为空 if (next == null) throw new ArgumentNullException(nameof(next)); if (schemes == null) throw new ArgumentNullException(nameof(schemes)); _next = next; Schemes = schemes; } public IAuthenticationSchemeProvider Schemes { get; set; } public async Task Invoke(HttpContext context) { // 保存原始请求路径信息到特性中,以便后续处理使用 context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature { OriginalPath = context.Request.Path, OriginalPathBase = context.Request.PathBase }); // 尝试获取所有实现了 IAuthenticationRequestHandler 的处理程序并执行 var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>(); foreach (var scheme in await Schemes.GetRequestHandlerSchemesA