ChallengeResult的作用

在ASP.NET Core的身份认证流程中,ChallengeResult 类的作用是作为一种特殊的返回结果类型,用于处理未经授权的HTTP请求。当控制器或中间件遇到需要进行身份验证的情况时(例如,一个受保护的API或路由要求用户登录),返回 ChallengeResult 就会触发框架内置的身份验证管道来执行相应的身份验证流程。

  1. 当控制器方法返回 ChallengeResult 时,可以指定相关的身份认证方案(如Cookie、JWT或其他自定义认证方案)
  2. ASP.NET Core身份认证中间件接收到 ChallengeResult 后,会按照配置的认证策略和Challenge参数来决定如何处理。对于基于浏览器的Web应用,这通常意味着重定向到登录页面或者显示身份提供者(如OAuth/OIDC)的授权界面。
  3. 用户完成登录过程后,身份认证系统将验证用户的凭据,并可能发放一个身份验证票据(如cookie或JWT token)。
  4. 最后,用户成功登录并验证后,系统将会自动重定向回之前尝试访问但未授权的原始URL。

ChangeResult的具体细节

继承ActionResult,重写了ExecuteResultAsync方法

ActionResult 在 ASP.NET Core MVC 框架中扮演着核心角色,它是控制器方法返回结果的基类。在ASP.NET Core MVC中,当请求到达控制器并执行完相应的方法后,框架通过 ControllerActionInvoker调用返回结果的 ExecuteResultAsync 方法来实际生成HTTP响应。这个方法由具体的结果类型实现,根据不同的结果类型产生不同的HTTP响应内容和状态码。

ControllerActionInvoker 具体的实现细节,触发的时机我暂时还未搞清楚

ChangeResult 继承了ActionResult,重写了ExecuteResultAsync方法,具体实现如下:

public override async Task ExecuteResultAsync(ActionContext context)
{
    ArgumentNullException.ThrowIfNull(context);
    var httpContext = context.HttpContext;
    var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
    var logger = loggerFactory.CreateLogger(typeof(ChallengeResult));
    Log.ChallengeResultExecuting(logger, AuthenticationSchemes);
    if (AuthenticationSchemes != null && AuthenticationSchemes.Count > 0)
    {
        foreach (var scheme in AuthenticationSchemes)
        {
            await httpContext.ChallengeAsync(scheme, Properties);
        }
    }
    else
    {
        await httpContext.ChallengeAsync(Properties);
    }
}

从源码可以分析得出一个信息,它调用了 httpContext.ChallengeAsync 方法来生成HTTP响应,ChallengeAsync是HttpContext的一个扩展方法。

AuthenticationHttpContextExtensions

AuthenticationHttpContextExtensions 是HttpContext的扩展方法类,提供了ChallengeAsync方法的实现,核心实现如下:

public static Task ChallengeAsync(this HttpContext context, string? scheme, AuthenticationProperties? properties) =>
    GetAuthenticationService(context).ChallengeAsync(context, scheme, properties);


private static IAuthenticationService GetAuthenticationService(HttpContext context) =>
    context.RequestServices.GetService<IAuthenticationService>() ??
        throw new InvalidOperationException(Resources.FormatException_UnableToFindServices(
            nameof(IAuthenticationService),
            nameof(IServiceCollection),
            "AddAuthentication"));

可以看出,其内部是使用 IAuthenticationService 来实现的,那么IAuthenticationService 的具体实现类是什么,又是在什么时候注册给services的呢?

AddAuthentication

在ASP.NET Core中,AddAuthenticationAuthenticationServiceCollectionExtensions类中提供的一个扩展方法,用于配置应用程序的身份验证服务。它通常在 Startup.cs 文件的 ConfigureServices 方法内调用,以启用身份验证中间件并设置默认的身份验证方案,这是我们一个比较熟悉的方法。

例如配置基于Cookie或JWT Bearer的身份验证服务,我们会按如下配置:

services.AddAuthentication(options => {
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie();

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "<your_authority_url>";
        options.Audience = "<your_audience>";
    });

AddAuthentication 的内部,其关键代码如下:

public static AuthenticationBuilder AddAuthentication(this IServiceCollection services)
{
    //其他代码省略
    services.AddAuthenticationCore();
    //其他代码省略
    return new AuthenticationBuilder(services);
}

AddAuthenticationCoreAuthenticationCoreServiceCollectionExtensions 类中提供的一个扩展方法,其关键代码如下:

public static IServiceCollection AddAuthenticationCore(this IServiceCollection services)
{
    //其他代码省略
    services.TryAddScoped<IAuthenticationService, AuthenticationService>();
    //其他代码省略
    return services;
}

至此,我们就已经搞清楚了IAuthenticationService 的具体实现类了,那就是AuthenticationService,并且知道了它是在 AddAuthentication 的时候注册给services的。

AuthenticationService

现在,让我们着重研究下AuthenticationService,分析其内部的代码:

public class AuthenticationService : IAuthenticationService
{
    //其他代码省略

    public IAuthenticationHandlerProvider Handlers { get; }

    //其他代码省略

    public virtual async Task ChallengeAsync(HttpContext context, string? scheme, AuthenticationProperties? properties)
    {
        if (scheme == null)
        {
            var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync();
            scheme = defaultChallengeScheme?.Name;
            if (scheme == null)
            {
                throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes     can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
            }
        }
        var handler = await Handlers.GetHandlerAsync(context, scheme);
        if (handler == null)
        {
            throw await CreateMissingHandlerException(scheme);
        }
        await handler.ChallengeAsync(properties);
    }
}

我们可以分析得出,其内部是通过调用IAuthenticationHandlerProviderGetHandlerAsync方法来获取一个IAuthenticationHandler的实例,然后调用其ChallengeAsync方法来生成HTTP响应,现在我们要搞清楚 IAuthenticationHandler 的具体实现类是什么,以及它在什么时候注册给services的呢?

身份认证方案与IAuthenticationHandler

在ASP.NET Core中,身份认证方案(Authentication Schemes)和 IAuthenticationHandler 接口紧密相关。身份认证方案是框架用来区分不同身份验证方式的一种机制,例如Cookie身份验证、JWT Bearer Token身份验证或OAuth 2.0身份验证等。

每个身份认证方案都与一个实现了 IAuthenticationHandler 接口的具体处理类相关联。IAuthenticationHandler 定义了身份验证过程中的关键方法,包括:

  1. AuthenticateAsync:负责实际的身份验证工作,即检查传入的HTTP请求并尝试确定用户身份。如果身份验证成功,则返回一个包含用户信息和其他认证细节的 AuthenticateResult 对象;否则返回未授权的结果。

  2. ChallengeAsync:当需要向客户端发出身份验证挑战时调用,通常会导致重定向到登录页面或者发送适当的HTTP响应头指示客户端进行身份验证。

  3. ForbidAsync:类似于 ChallengeAsync,但在已知用户身份但权限不足的情况下使用,同样会生成相应的拒绝访问响应。

  4. SignInAsync 和 SignOutAsync:分别用于创建和注销用户的会话,如设置身份验证cookie或将cookie清除。

在配置身份认证服务时,通过调用 AuthenticationBuilder.AddScheme<TOptions, THandler>(string, Action<TOptions>) 方法注册具体的身份验证处理器实现(这些实现通常会封装 IAuthenticationHandler 的行为),然后在 AddAuthentication 中指定默认或其他自定义的身份认证方案。这样,当应用运行时,根据请求上下文和配置的方案,ASP.NET Core中间件将调用对应方案的 IAuthenticationHandler 来执行身份验证流程。

现在,让我们回到上文 AddAuthentication 的提到的代码:

services.AddAuthentication(options => {
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie();

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "<your_authority_url>";
        options.Audience = "<your_audience>";
    });

我们可以看到,指定了Cookie和JWT Bearer Token两种身份认证方案,但是我们并没有看到它们是如何绑定身份认证方案处理器的。实际上,绑定身份认证方案处理器的操作就在 AddCookieAddJwtBearer 中,AddCookieAddJwtBearer 都是在 ASP.NET Core 中基于 AddScheme 进行扩展和注册认证方案的方式,它们的源码类似如下:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options => { ... });

// 相当于内部执行了 AddScheme 关联了 Cookie 的身份认证方案处理器类
services.AddAuthentication(options =>
{
    options.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>(
        CookieAuthenticationDefaults.AuthenticationScheme, configureOptions => { ... });
});

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => { ... });

// 相当于内部执行了 AddScheme 关联了 JwtBearer 的身份认证方案处理器类
services.AddAuthentication(options =>
{
    options.AddScheme<JwtBearerOptions, JwtBearerHandler>(
        JwtBearerDefaults.AuthenticationScheme, configureOptions => { ... });
});

因为我们在 Startup.cs 中调用了 AddAuthentication 方法,并绑定了身份认证方案和对应的具体身份认证方案处理器,所以在 AuthenticationService 类中,我们就能够根据请求上下文,调用身份认证方案对应的身份认证方案处理器类来处理 ChallengeAsync 了。

身份认证方案处理器(IAuthenticationHandler的实现类)

根据上文,我们已经知道了身份认证方案处理器和身份认证方案的关系了,同时也知道它们是在什么时候注册到services的,那么现在以 CookieAuthenticationHandler 为例,看看 CookieAuthenticationHandlerChallengeAsync 的具体实现。

protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
    var redirectUri = properties.RedirectUri;
    if (string.IsNullOrEmpty(redirectUri))
    {
        redirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
    }
    var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri);
    var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(loginUri));
    await Events.RedirectToLogin(redirectContext);
}

注:CookieAuthenticationHandler 继承了抽象类 AuthenticationHandler,抽象类中提供了 ChallengeAsyncHandleForbiddenAsync 方法,ChallengeAsync 内部会调用HandleForbiddenAsync 方法,所以我们只需要关注 HandleChallengeAsync 的重写实现即可。

不难看出,当我们用Cookie身份认证方案时,如果控制返回 ChallengeResult 时,会重定向到配置的登录页面中,这个Options 配置在 AddCookie 的时候可以指定。 当我们自定义身份认证方案处理器时,也可以参照重写 HandleChallengeAsync 方法,做自定义的重定向或其他的一些操作。

总结

虽然 ChangeResult 的内部机制比较复杂,但我们可以简单的进行理解为:当控制器返回 ChangeResult后,ASP.NET Core 框架会自动根据 ChangeResult 参数指定的身份认证方案,找到对应的身份认证方案处理器(IAuthenticationHandler的实现类)进行处理,我们只需要找到对应的身份认证方案处理器即可知道最终HTTP响应结果,也可以根据需要进行自定义处理。

ChangeResult 类似的 ForbidResult 也基本上是这样。