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

1.前言

上一篇我们实战了怎么用 JWT 认证和 Cookie 认证,不得不感叹这套东西设计得很巧妙——我们只要简单配几行,一整套完整的功能就能跑起来。那这一篇,我们就往下钻一层,看看框架到底是怎么实现的。我大概列了 3 个方向入手。在正式开始之前,我先说下按这几个问题去理解,会顺很多。

认证模块是怎么注册的?

第一部分我们看它是如何注册的、注册了哪些东西,代码里是通过 AddAuthentication() 来完成的。

认证方案是怎么接入的?

第二部分我们挑一个具体的认证方案(Scheme),看看它如何在认证注册的基础上,把一个方案添加进去,代码里是通过 AddScheme() 完成的。

请求是怎么被认证的?

第三部分我们看一个 HTTP 请求最终被谁处理、并且怎么判断是否认证通过。代码里主要靠 app.UseAuthentication() 来配置,不加这句,HTTP 请求进来就不会走认证流程。

小提示:本文看的源码是基于 .NET Core 7.0 的,跟最新版本可能有些出入,但差异很小,不影响整体流程的理解。

2.认证模块的注册

这一步的本质,是注册认证系统赖以生存的核心基础设施,它不涉及任何具体的认证逻辑(比如怎么读 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

如果说 AddDataProtectionISystemClock 是保证引擎正常运转的润滑油和电源,那么 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<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 方案。

它主要存三部分信息:

  • 默认方案:如 DefaultAuthenticateSchemeDefaultChallengeSchemeDefaultSignInSchemeDefaultSignOutSchemeDefaultForbidScheme
  • 方案列表:所有已注册方案的名称和显示名称。
  • 方案与 Handler 的映射:每个方案名称对应一个 AuthenticationSchemeBuilder,其中包含了 HandlerType

它本身只是数据的容器,具体的注册逻辑由 AddScheme 操作它,具体的查询逻辑由 IAuthenticationSchemeProvider 读取它。

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

IAuthenticationSchemeProvider 负责管理所有注册过的认证方案(Scheme),默认实现是 AuthenticationSchemeProvider。当调用 AddCookieAddJwtBearer 注册方案时,这些方案信息(比如名字叫 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<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

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 通过下面这段:

Services.Configure<AuthenticationOptions>(o =>
{
    o.AddScheme(authenticationScheme, scheme =>
    {
        scheme.HandlerType = state.HandlerType;
        scheme.DisplayName = displayName;
    });
});

把当前方案信息写入全局配置对象 AuthenticationOptions。看 AuthenticationOptions

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 做了这么几件事:

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

image

  1. 把 Builder 同时加到 _schemes 列表和 SchemeMap 字典中,以方案名称为键。
  2. 如果调用 AddJwtBearer 时传了 configureOptions 委托,那么此时也会把用于配置 Issuer、Audience 的委托绑定到对应的方案名称上,让后面解析 JwtBearerOptions 时能按方案名称拿到对应的配置实例。

最后再调用 Services.AddTransient<JwtBearerHandler>(),把具体的认证处理器 JwtBearerHandler 注册到 DI 容器,每次请求需要时重新实例化。

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

3.4 方案注册小结

先看整体流程图:

image

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

  • AuthenticationOptionsSchemeMap 字典,存储了所有注册方案的名称、显示名称和对应的 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.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 记录原始请求

context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
{
    OriginalPath = context.Request.Path,
    OriginalPathBase = context.Request.PathBase
});

请求一到认证中间件,就立刻把当前的 URL 路径存进 AuthenticationFeature。因为后续某些认证方案(例如 OAuth 的回调)可能会修改或重写路径,保存原始路径能保证:即便路径被改过,也能找回用户最初访问的是哪个地址。

4.2 远程认证优先执行

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 为例,我们需要这样配置:

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,一定知道我在说什么。

后端模式

image

前端模式

image

.NET Core 其实默认集成了好几种第三方方式,但我们目前还没讲第三方登录怎么做。而且我觉得,要理解三方登录或 OIDC,必须先建立在了解 OAuth 2.0 的基础上,才能理解得更透,所以这里就不多展开了。

4.3 执行核心认证逻辑

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(授权中间件)的兄弟。后面会单独研究它。

认证中间件核心流程图

image

4.4 到底是怎么认证的?

我们接着往下深入:

var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
var result = await context.AuthenticateAsync(defaultAuthenticate.Name);

看看这段代码内部做了什么:

  • 从中间件构造时拿到的 IAuthenticationSchemeProvider 对象中,获取注册时的默认鉴权方案。
  • 用这个默认方案进行鉴权,前面说了它最终会走到 IAuthenticationService.AuthenticateAsync

那么接下来我们就该进到 IAuthenticationServiceAuthenticateAsync 里,它的默认实现是 AuthenticationService(可以回到第 2 节复习)。

// 负责管理注册的认证方案(如 "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>

这段逻辑其实就三步:

  1. 如果没传入 scheme 方案名,就默认用 IAuthenticationSchemeProvider 获取当前默认的 Scheme。
  2. 把方案名给到 IAuthenticationHandlerProvider,拿到该方案在注册时对应的 Handler。
  3. 用这个具体的 Handler 执行认证。

5.总结

到这里,我们已经完整走完了 .NET Core 中「认证与方案的注册」以及「认证请求在中间件里的执行」这两条链路。最后按三层再串一遍。

5.1 基础设施层

调用 AddAuthentication() 时,系统首先注册了认证运行所必需的底层组件:

  • 核心服务IAuthenticationServiceIAuthenticationSchemeProviderIAuthenticationHandlerProvider,定义了认证的标准流程——登录、登出、认证、质询、禁止。
  • 辅助组件:数据保护、时间、配置提供者、URL 编码,为具体方案提供加密、时间校验、配置读取等通用能力。

5.2 方案注册层

调用 AddCookie()AddJwtBearer(),通过 AddScheme 把具体方案的 Handler 和 Options 注册进 AuthenticationOptionsSchemeMap 字典:

  • Handler(如 CookieAuthenticationHandlerJwtBearerHandler):负责实际的凭证解析和验证。
  • Options(如 CookieAuthenticationOptionsJwtBearerOptions):存储方案特有的配置,如过期时间、密钥、回调路径。

注册完成后,IAuthenticationSchemeProvider 能查询所有已注册的认证方案,再由 IAuthenticationHandlerProvider 从容器里按需实例化对应的 Handler。

5.3 请求执行层

UseAuthentication() 注册的 AuthenticationMiddleware 在请求管道中执行时:

  • 记录原始路径到 AuthenticationFeature,方便后续远程认证回调使用。
  • 优先执行 IAuthenticationRequestHandler(如 OAuth 回调),若 Handler 自行处理了请求(重定向 / 直接响应),就直接返回、中断中间件的执行。
  • 对普通 HTTP 请求,先获取默认认证方案,再走 AuthenticationService.AuthenticateAsync → Handler.AuthenticateAsync 解析,然后把解析出的 ClaimsPrincipal 赋给 HttpContext.User

不管认证成功或失败,请求都会继续向下执行,不会在这个中间件里返回 401/403——认证中间件只负责给请求「贴一张(认证 / 未认证的)票据标签」,真正的授权拦截(401/403),交给后面的 AuthorizationMiddleware 来负责实现。

posted @ 2026-07-06 22:36  yuyuyui  阅读(0)  评论(0)    收藏  举报