深入理解ASP.NET Core中的自定义身份验证中间件

在开发过程中,经常会遇到需要将系统嵌入到客户系统中的场景。这意味着需要将JavaScript包集成到他们的页面中,并与基于用户的系统进行通信。为了实现这一点,可能会考虑将应用程序拆分为两个独立的应用:主应用和集成部分。然而,这种方法在维护性、可测试性、可扩展性和环境设置方面存在诸多问题。为了解决这些问题,探索了一种新的方法,即使用SignalR和身份验证来实现主应用和客户端集成侧的相同控制和行为。

目标是创建一个React控件,它可以通过SignalR从不同的域与用户应用程序进行通信。为了实现这一点,需要配置控件,以便它可以与系统建立联系,同时让知道它来自哪个环境,以便可以决定身份验证的方式。

在实现过程中,首先考虑了使用IdentityServer 4,但后来发现客户要求从他们的系统中进行身份验证,而他们使用的是WCF,这使得IdentityServer 4不再适用。接下来,尝试了使用身份验证处理程序,但这是一个深不可测的兔子洞,没有带来太多价值。在时间压力下,最终选择了深入研究AuthenticationMiddleware。

AuthenticationMiddleware的代码如下所示(请更多地关注行为而不是代码本身,因为反编译的代码并不总是看起来像最初编写的那样): public class AuthenticationMiddleware { private readonly RequestDelegate _next; public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes) { _next = next ?? throw new ArgumentNullException(nameof(next)); Schemes = schemes ?? throw new ArgumentNullException(nameof(schemes)); } public IAuthenticationSchemeProvider Schemes { get; set; } public async Task Invoke(HttpContext context) { context.Features.Set(new AuthenticationFeature { OriginalPath = context.Request.Path, OriginalPathBase = context.Request.PathBase }); IAuthenticationHandlerProvider handlers = context.RequestServices.GetRequiredService(); foreach (AuthenticationScheme authenticationScheme in await schemes.GetRequestHandlerSchemesAsync()) { IAuthenticationRequestHandler handlerAsync = await handlers.GetHandlerAsync(context, authenticationScheme.Name) as IAuthenticationRequestHandler; bool flag = handlerAsync != null; if (flag) flag = await handlerAsync.HandleRequestAsync(); if (flag) return; } AuthenticationScheme authenticateSchemeAsync = await schemes.GetDefaultAuthenticateSchemeAsync(); if (authenticateSchemeAsync != null) { AuthenticateResult authenticateResult = await context.AuthenticateAsync(authenticateSchemeAsync.Name); if (authenticateResult?.Principal != null) context.User = authenticateResult.Principal; } await _next(context); } } 在上述代码中,最重要的一行用于身份验证是context.User = authenticateResult.Principal;,因为这实际上设置了在整个请求过程中要使用的user。

遗憾的是,Invoke方法不是虚拟的,唯一的选择是将其原样粘贴到代码库中,重命名以避免混淆,然后对其进行调整以满足需求。修改后的代码如下所示: public class CustomAuthenticationMiddleware { private readonly RequestDelegate _next; private readonly IConfiguration _configuration; private readonly IServiceProvider _serviceProvider; public CustomAuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes, IConfiguration configuration, IServiceProvider serviceProvider) { _next = next ?? throw new ArgumentNullException(nameof(next)); _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); Schemes = schemes ?? throw new ArgumentNullException(nameof(schemes)); _serviceProvider = serviceProvider; } public IAuthenticationSchemeProvider Schemes { get; set; } public async Task Invoke(HttpContext context) { context.Features.Set(new AuthenticationFeature { OriginalPath = context.Request.Path, OriginalPathBase = context.Request.PathBase }); if (context.Request.Path.Value.Contains("someArbitraryPath")) { IServiceProvider scopedServiceProvider = _serviceProvider.CreateScope().ServiceProvider; IUserClaimsPrincipalFactory claimsPrincipalFactory = scopedServiceProvider.GetRequiredService(); ApplicationUser user = new ApplicationUser(); // here you would get the actual user from your system; context.User = await claimsPrincipalFactory.CreateAsync(user); } else { IAuthenticationHandlerProvider handlers = context.RequestServices.GetRequiredService(); foreach (AuthenticationScheme authenticationScheme in await schemes.GetRequestHandlerSchemesAsync()) { IAuthenticationRequestHandler handlerAsync = await handlers.GetHandlerAsync(context, authenticationScheme.Name) as IAuthenticationRequestHandler; bool flag = handlerAsync != null; if (flag) flag = await handlerAsync.HandleRequestAsync(); if (flag) return; } AuthenticationScheme authenticateSchemeAsync = await schemes.GetDefaultAuthenticateSchemeAsync(); if (authenticateSchemeAsync != null) { AuthenticateResult authenticateResult = await context.AuthenticateAsync(authenticateSchemeAsync.Name); if (authenticateResult?.Principal != null) context.User = authenticateResult.Principal; } } await _next(context); } } 在这个示例中,注入了IConfiguration和IServiceProvider,因为场景需要为不同的环境配置,并且希望使用服务提供商中的存储库和自定义服务。

需要注意的一点是,代码中有一行_serviceProvider.CreateScope().ServiceProvider;,因为发现有些服务不能从根ServiceProvider创建,所以为了使其工作,需要创建一个特定的作用域来检索它们。

在实现自定义身份验证中间件时,需要考虑以下几点: 可以引入任何想要的自定义逻辑,使其可配置,并根据传入的请求做出决策,包括cookies、headers以及可能到达的任何其他内容。 这不会从HttpContext的角度影响应用程序的其他部分,或者任何控制器,用户就像任何其他用户一样有效且经过身份验证。 由于这不像正常的身份验证那样基于cookie,需要确保正在执行的检查非常快速且非常特定,就像在例子中,一旦SignalR协商完成,如果最终使用WebSockets,它就不太可能再次运行(假设,可能是错的)。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485