1.前言
上一篇我们实战了怎么用 JWT 认证和 Cookie 认证,不得不感叹这套东西设计得很巧妙------我们只要简单配几行,一整套完整的功能就能跑起来。那这一篇,我们就往下钻一层,看看框架到底是怎么实现的。我大概列了 3 个方向入手。在正式开始之前,我先说下按这几个问题去理解,会顺很多。
认证模块是怎么注册的?
第一部分我们看它是如何注册的、注册了哪些东西,代码里是通过 AddAuthentication() 来完成的。
认证方案是怎么接入的?
第二部分我们挑一个具体的认证方案(Scheme),看看它如何在认证注册的基础上,把一个方案添加进去,代码里是通过 AddScheme() 完成的。
请求是怎么被认证的?
第三部分我们看一个 HTTP 请求最终被谁处理、并且怎么判断是否认证通过。代码里主要靠 app.UseAuthentication() 来配置,不加这句,HTTP 请求进来就不会走认证流程。
小提示:本文看的源码是基于 .NET Core 7.0 的,跟最新版本可能有些出入,但差异很小,不影响整体流程的理解。
2.认证模块的注册
这一步的本质,是注册认证系统赖以生存的核心基础设施 ,它不涉及任何具体的认证逻辑(比如怎么读 Cookie、怎么解 JWT),而是定义认证系统应该怎么运转。
前面介绍过 Cookie 和 JWT 认证是怎么在代码里集成的,它们有个共同点:在启动类集成时都会加上这么一句。其实不光这两个,任何认证方案都离不开下面这段代码。
csharp
var authenticationBuilder = builder.Services.AddAuthentication(options =>
{
});
看名字就知道它是「添加身份认证」的意思,返回一个 AuthenticationBuilder。那它到底添加了什么?它是一个容器的扩展方法,我们直接进它内部看实现。
2.1 基础设施层
先看 AuthenticationServiceCollectionExtensions:
csharp
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 数据、编解码令牌相关数据。
可以这么理解:加密保护、配置读取、核心服务、时间校验、数据编码这几块凑在一起,共同构成了认证模块这套基础设施。

2.2 核心引擎 AddAuthenticationCore
如果说 AddDataProtection 和 ISystemClock 是保证引擎正常运转的润滑油和电源,那么 AddAuthenticationCore 就是这台引擎的缸体和曲轴------它定义了动力产生的核心逻辑。
所以这部分我们主要围绕 AddAuthenticationCore 展开。用红花绿叶来打比方,AddAuthenticationCore 就是那朵红花,其他几个是衬托它的绿叶,但绿叶也缺一不可。理解成「引擎」更贴切,后面顺带也会简单聊下 IAuthenticationConfigurationProvider。
看 AuthenticationCoreServiceCollectionExtensions:
csharp
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 的认证舞台就具备了运转能力。

(1)认证入口 IAuthenticationService
这是认证系统统一对外的接口,默认实现是 AuthenticationService,定义了 5 个核心方法:
csharp
public interface IAuthenticationService
{
// 1. 认证:获取请求的任何认证数据(如解析 Cookie/Token 得到用户身份)
Task<AuthenticateResult> 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 类型是什么)都会被存放到类似这样的字典里:
csharp
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:
csharp
// 添加 JWT 方案
public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string? displayName, Action<JwtBearerOptions> configureOptions)
{
// 用于从配置文件中自动绑定基础选项
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<JwtBearerOptions>, 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:
csharp
private AuthenticationBuilder AddSchemeHelper<TOptions, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>(
string authenticationScheme,
string? displayName,
Action<TOptions>? 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<TOptions>类型的派生类。
这里说「派生类」而不是「子类」,是因为中间隔着几层继承也是允许的。
接着看,AddScheme 最终调用了私有方法 AddSchemeHelper。它的第一个动作是实例化一个 AddSchemeHelperState 对象,把 THandler(也就是 JwtBearerHandler)的类型信息存进它的 HandlerType 属性。这个对象的作用,是在后面的委托链里保留 Handler 的类型信息------因为 Configure 委托是延后执行的,得先把类型捕获下来。
3.3 AuthenticationOptions 注册方案映射
AddSchemeHelper 通过下面这段:
csharp
Services.Configure<AuthenticationOptions>(o =>
{
o.AddScheme(authenticationScheme, scheme =>
{
scheme.HandlerType = state.HandlerType;
scheme.DisplayName = displayName;
});
});
把当前方案信息写入全局配置对象 AuthenticationOptions。看 AuthenticationOptions:
csharp
public class AuthenticationOptions
{
private readonly IList<AuthenticationSchemeBuilder> _schemes = new List<AuthenticationSchemeBuilder>();
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属性。

- 把 Builder 同时加到
_schemes列表和SchemeMap字典中,以方案名称为键。 - 如果调用
AddJwtBearer时传了configureOptions委托,那么此时也会把用于配置 Issuer、Audience 的委托绑定到对应的方案名称上,让后面解析JwtBearerOptions时能按方案名称拿到对应的配置实例。
最后再调用 Services.AddTransient<JwtBearerHandler>(),把具体的认证处理器 JwtBearerHandler 注册到 DI 容器,每次请求需要时重新实例化。
这一步走完,AuthenticationOptions 内部维护的 SchemeMap 字典里,就包含了方案名称 和 AuthenticationSchemeBuilder 的映射关系。
3.4 方案注册小结
先看整体流程图:

整个注册流程走完后,依赖注入容器里就有了:
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>
csharp
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.GetRequestHandlerSchemesAsync())
{
var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
// 如果处理程序成功处理了请求(例如重定向到登录页),则直接返回,不再继续管道
if (handler != null && await handler.HandleRequestAsync()) return;
}
// 获取默认的认证方案并尝试进行认证
var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
if (defaultAuthenticate != null)
{
var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
// 如果认证成功且包含 Principal,将其赋值给 HttpContext.User
if (result?.Principal != null) context.User = result.Principal;
// 如果认证完全成功,设置相关的认证特性到 HttpContext.Features 中
if (result?.Succeeded ?? false)
{
var authFeatures = new AuthenticationFeatures(result);
context.Features.Set<IHttpAuthenticationFeature>(authFeatures);
context.Features.Set<IAuthenticateResultFeature>(authFeatures);
}
}
// 调用管道中的下一个中间件
await _next(context);
}
}
4.1 记录原始请求
csharp
context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
{
OriginalPath = context.Request.Path,
OriginalPathBase = context.Request.PathBase
});
请求一到认证中间件,就立刻把当前的 URL 路径存进 AuthenticationFeature。因为后续某些认证方案(例如 OAuth 的回调)可能会修改或重写路径,保存原始路径能保证:即便路径被改过,也能找回用户最初访问的是哪个地址。
4.2 远程认证优先执行
csharp
foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
{
var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
if (handler != null && await handler.HandleRequestAsync()) return;
}
抢在所有认证逻辑之前,先检查有没有特殊方案实现了 IAuthenticationRequestHandler 接口。如果某个 Handler 的 HandleRequestAsync() 返回 true,说明它已经自行处理了本次请求(比如直接重定向到了登录页,或者直接返回了 401 响应),就直接 return,不再执行后面任何认证逻辑,也不再调用 _next 传给下一个中间件。
最实际的应用场景,就是处理需要与外部服务器交互的认证流程,例如 OAuth 2.0 或 OIDC,或者用微信、GitHub、Google 登录。以 OIDC 为例,我们需要这样配置:
csharp
services.AddAuthentication()
.AddOpenIdConnect(options =>
{
// 1. 配置第三方服务的地址
options.Authority = "https://yuxl.com"; // 第三方服务的地址
options.ClientId = "testAppid";
options.ClientSecret = "TestSecretKey";
// 2. 配置回调路径,路径必须和在第三方平台注册的「重定向 URI」一样
options.CallbackPath = "/signin-oidc";
})
我们在这里配了回调地址,那当第三方回调这个接口时,就会默认被 IAuthenticationRequestHandler 执行,它的默认实现是 RemoteAuthenticationHandler。它内部会读取回调的授权码去换取 Token,或者直接读到 Token 后换取用户信息,然后触发本地登录;登录完成后,把浏览器重定向回你发起登录时指定的原始 URL,用户感觉就是「登录成功后回到了网站」。
不过上面这套流程,在服务端渲染(SSR)架构里比较适合,例如 MVC 和 Razor Pages;在前后端分离模式下就不太合适了。因为前后端分离下如果要实现基于 OAuth 2.0 的三方登录,那两次重定向基本是前端来完成的------描述可能不太严谨,但你要是了解过 OAuth 2.0,一定知道我在说什么。
后端模式

前端模式

.NET Core 其实默认集成了好几种第三方方式,但我们目前还没讲第三方登录怎么做。而且我觉得,要理解三方登录或 OIDC,必须先建立在了解 OAuth 2.0 的基础上,才能理解得更透,所以这里就不多展开了。
4.3 执行核心认证逻辑
csharp
var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
if (defaultAuthenticate != null)
{
var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
if (result?.Principal != null) context.User = result.Principal;
// 如果认证完全成功,设置相关的认证特性到 HttpContext.Features 中
if (result?.Succeeded ?? false)
{
var authFeatures = new AuthenticationFeatures(result);
context.Features.Set<IHttpAuthenticationFeature>(authFeatures);
context.Features.Set<IAuthenticateResultFeature>(authFeatures);
}
}
// 调用管道中的下一个中间件
await _next(context);
从配置里读取 DefaultAuthenticateScheme(看是 Cookies 还是 JwtBearer),然后传入方案名调用 context.AuthenticateAsync() 去认证。它内部靠 IAuthenticationService.AuthenticateAsync 真正解析请求里的凭证------读取 Cookie、解 JWT 等等。
从代码能看出来,这一步只负责「读和解析」:成功就把认证结果(身份信息)写到上下文的 User 里,失败就不写身份。但无论成功还是失败,管道都会继续走向下一个中间件,不会在这里直接返回 401。
由此可以得出一个结论:
对于非第三方认证的请求,认证中间件只负责「贴标签」,绝对不会拦截请求。真正判断标签是匿名 / 失效、并拦下请求返回 401/403 的,是后面那个叫
AuthorizationMiddleware(授权中间件)的兄弟。后面会单独研究它。
认证中间件核心流程图

4.4 到底是怎么认证的?
我们接着往下深入:
csharp
var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
看看这段代码内部做了什么:
- 从中间件构造时拿到的
IAuthenticationSchemeProvider对象中,获取注册时的默认鉴权方案。 - 用这个默认方案进行鉴权,前面说了它最终会走到
IAuthenticationService.AuthenticateAsync。
那么接下来我们就该进到 IAuthenticationService 的 AuthenticateAsync 里,它的默认实现是 AuthenticationService(可以回到第 2 节复习)。
csharp
// 负责管理注册的认证方案(如 "Cookie"、"Bearer")
public interface IAuthenticationSchemeProvider
{
Task<AuthenticationScheme?> GetDefaultAuthenticateSchemeAsync();
Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync();
}
// 负责根据方案名称解析出具体的 IAuthenticationHandler 实例
public interface IAuthenticationHandlerProvider
{
Task<IAuthenticationHandler?> GetHandlerAsync(HttpContext context, string schemeName);
}
// 用于在认证成功后修改 ClaimsPrincipal(例如添加额外用户角色)
public interface IClaimsTransformation
{
Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal);
}
// 核心认证逻辑实现
public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string? scheme)
{
// 1. 如果未指定方案,获取默认的认证方案
if (scheme == null)
{
var defaultScheme = await Schemes.GetDefaultAuthenticateSchemeAsync();
scheme = defaultScheme?.Name;
if (scheme == null)
{
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found.");
}
}
// 2. 通过 HandlerProvider 获取具体的处理器
var handler = await Handlers.GetHandlerAsync(context, scheme);
if (handler == null)
{
throw await CreateMissingHandlerException(scheme);
}
// 3. 调用处理器的 AuthenticateAsync 方法执行具体认证
// Handlers should not return null, but we'll be tolerant of null values for legacy reasons.
var result = (await handler.AuthenticateAsync()) ?? AuthenticateResult.NoResult();
// 4. 如果认证成功,进行 Claims 转换(如角色添加等)
if (result.Succeeded)
{
var principal = result.Principal!;
var doTransform = true;
_transformCache ??= new HashSet<ClaimsPrincipal>();
// 简单的缓存逻辑,防止重复转换
if (_transformCache.Contains(principal))
{
doTransform = false;
}
if (doTransform)
{
principal = await Transform.TransformAsync(principal);
_transformCache.Add(principal);
}
// 返回成功的结果
return AuthenticateResult.Success(new AuthenticationTicket(principal, result.Properties, result.Ticket!.AuthenticationScheme));
}
return result;
}
源码太长,这里只列关键代码,感兴趣的可以去看完整版:< https://source.dot.net/#Microsoft.AspNetCore.Authentication.Core/AuthenticationService.cs ,30159f972b8c22ae>
这段逻辑其实就三步:
- 如果没传入
scheme方案名,就默认用IAuthenticationSchemeProvider获取当前默认的 Scheme。 - 把方案名给到
IAuthenticationHandlerProvider,拿到该方案在注册时对应的 Handler。 - 用这个具体的 Handler 执行认证。
5.总结
到这里,我们已经完整走完了 .NET Core 中「认证与方案的注册」以及「认证请求在中间件里的执行」这两条链路。最后按三层再串一遍。
5.1 基础设施层
调用 AddAuthentication() 时,系统首先注册了认证运行所必需的底层组件:
- 核心服务 :
IAuthenticationService、IAuthenticationSchemeProvider、IAuthenticationHandlerProvider,定义了认证的标准流程------登录、登出、认证、质询、禁止。 - 辅助组件:数据保护、时间、配置提供者、URL 编码,为具体方案提供加密、时间校验、配置读取等通用能力。
5.2 方案注册层
调用 AddCookie() 或 AddJwtBearer(),通过 AddScheme 把具体方案的 Handler 和 Options 注册进 AuthenticationOptions 的 SchemeMap 字典:
- Handler (如
CookieAuthenticationHandler、JwtBearerHandler):负责实际的凭证解析和验证。 - Options (如
CookieAuthenticationOptions、JwtBearerOptions):存储方案特有的配置,如过期时间、密钥、回调路径。
注册完成后,IAuthenticationSchemeProvider 能查询所有已注册的认证方案,再由 IAuthenticationHandlerProvider 从容器里按需实例化对应的 Handler。
5.3 请求执行层
UseAuthentication() 注册的 AuthenticationMiddleware 在请求管道中执行时:
- 记录原始路径到
AuthenticationFeature,方便后续远程认证回调使用。 - 优先执行
IAuthenticationRequestHandler(如 OAuth 回调),若 Handler 自行处理了请求(重定向 / 直接响应),就直接返回、中断中间件的执行。 - 对普通 HTTP 请求,先获取默认认证方案,再走
AuthenticationService.AuthenticateAsync → Handler.AuthenticateAsync解析,然后把解析出的ClaimsPrincipal赋给HttpContext.User。
不管认证成功或失败,请求都会继续向下执行,不会在这个中间件里返回 401/403------认证中间件只负责给请求「贴一张(认证 / 未认证的)票据标签」,真正的授权拦截(401/403),交给后面的
AuthorizationMiddleware来负责实现。