65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET Core 2.1 中的分支身份验证

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2018年11月13日

CPOL

5分钟阅读

viewsIcon

5211

ASP.NET Core 2.1 中的分支身份验证

引言

在上一篇博文中,我曾暗示过使用 SignalR 和查询字符串来分支用户的身份验证方式。

背景故事

我陷入这个场景是因为系统的一个需求是需要将其嵌入到客户的系统中。这意味着我们会将某个 JavaScript 包集成到他们的页面中,并且它需要与我们的用户系统进行通信。

最快捷的解决方案(尽管可维护性最差,只是想出来的最省力的方法)是拥有 2 个独立的应用来分别处理,一个是我们主应用,另一个是集成部分。这种方法不可维护的原因如下:

  • 测试和调试时间。当出现问题时,我们需要增加维护工作量,并验证两个系统的更改。
  • 缺乏可扩展性。通过这种方法,如果未来的客户需要不同的集成方式,就需要另一个应用程序。
  • 自动化测试。当然,我们可以测试通用代码,但会有太多部分会发散,测试分支将成为系统的负担。
  • 环境设置。我们讨论的是集成和独立应用程序,同时运行的应用程序数量有限,而且还要保持思路清晰。

因此,我着手寻找一种方法,让 SignalR 和身份验证协同工作,以便我们能够维护现有应用程序,在主应用和客户端集成方都具有相同的控制和行为,并且客户端确实能够工作,这与其说是一种选择加入的过程。

为了了解我们构建了什么以及如何构建,我们的控件最终是一个 React 控件(这样,本地测试和样式化都更容易),它通过 SignalR 从不同的域与我们的用户系统通信。

这就是上一篇博文的由来,因为我们希望配置我们的控件以访问我们的系统,同时还让我们知道它来自哪个环境,以便我们能够决定身份验证方式。

现在回到我们的主题

因此,我们看到了如何通过查询字符串将额外数据发送到 SignalR 以提供有关预期内容的更多信息,下一个问题是,我们如何利用这一点?

完全披露,由于 ASP.NET Core 是如此可扩展,并且存在于许多不同的包中,要找到特定类的源代码很困难,至少对我来说是这样,所以反编译器很有用,但这并不意味着反编译器应该用于恶意目的。我主要在想要比现有文档(如果存在的话)更好地理解底层系统时使用它们,那是我的真相来源。

考虑的选项

第一个想到的可能是 IdentityServer 4,它奏效了一段时间,直到我们发现客户要求我们从他们的系统进行身份验证,并且他们正在为此使用 WCF,所以那个方案失败了。

接下来,尝试使用身份验证处理程序,那是一个深不可测的兔子洞,并没有带来太多价值(我有没有提到时间压力?😀),不是说它没有用,或者不起作用,只是太耗时且失败的测试太多,无法继续。

解决方案

最后,当前的方法(未来可能还有更好的方法,谁知道呢,很有趣,对吧? :D)是深入研究 AuthenticationMiddleware

AuthenticationMiddleware 的代码如下(请更多地关注行为而不是代码,请记住反编译的代码并不总是与原始编写的代码相同)

public class AuthenticationMiddleware
{
    private readonly RequestDelegate _next;

    public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider chemes)
    {
        _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 = ontext.RequestServices.GetRequiredService();
        foreach (AuthenticationScheme authenticationScheme 
                 in await chemes.GetRequestHandlerSchemesAsync())
        {
            IAuthenticationRequestHandler handlerAsync = await handlers.GetHandlerAsynccontext, 
                                  authenticationScheme.Name) as IAuthenticationRequestHandler;
            bool flag = handlerAsync != null;
            if (flag)
                flag = await handlerAsync.HandleRequestAsync();
            if (flag)
                return;
        }
        AuthenticationScheme authenticateSchemeAsync = 
                                  await chemes.GetDefaultAuthenticateSchemeAsync();
        if (authenticateSchemeAsync != null)
        {
            AuthenticateResult authenticateResult = 
                                  await context.AuthenticateAsyncauthenticateSchemeAsync.Name);
            if (authenticateResult?.Principal != null)
                context.User = authenticateResult.Principal;
        }
        await _next(context);
    }
}

查看上面的代码,对于身份验证最重要的行是 context.User = authenticateResult.Principal;,因为它实际上设置了整个请求中使用的用户。

遗憾的是,正如我们所见,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(nameofconfiguration));
        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")) // and any other 
                                                                      // conditions we wish
        {
            IServiceProvider scopedServiceProvider = _serviceProvider.CreateScope().ServiceProvider;
            IUserClaimsPrincipalFactory claimsPrincipalFactory =
                scopedServiceProvider.GetRequiredService<IUserClaimsPrincipalFactory>();
            ApplicationUser user = new ApplicationUser(); // here you would get the 
                                                          // actual user from your system;
            context.User = await claimsPrincipalFactory.CreateAsync(user);
        }
        else
        {
            IAuthenticationHandlerProvider handlers = ontext.RequestServices.GetRequiredService);
            foreach (AuthenticationScheme authenticationScheme 
                                in await chemes.GetRequestHandlerSchemesAsync())
            {
                IAuthenticationRequestHandler handlerAsync = 
                await andlers.GetHandlerAsync(context, authenticationScheme.Name) 
                                as AuthenticationRequestHandler;
                bool flag = handlerAsync != null;
                if (flag)
                    flag = await handlerAsync.HandleRequestAsync();
                if (flag)
                    return;
            }
            AuthenticationScheme authenticateSchemeAsync = 
                                    await chemes.GetDefaultAuthenticateSchemeAsync();
            if (authenticateSchemeAsync != null)
            {
                AuthenticateResult authenticateResult = 
                                    await context.AuthenticateAsyncauthenticateSchemeAsync.Name);
                if (authenticateResult?.Principal != null)
                    context.User = authenticateResult.Principal;
            }
        }

        await _next(context);
    }
}

在这个示例中,我们注入了 IConfigurationIServiceProvider,因为在我们的场景中,我们需要为不同的环境配置设置,并且我们还希望使用服务提供商中的存储库和自定义服务。

这里需要知道的一点是,在说 _serviceProvider.CreateScope().ServiceProvider; 的那一行,因为我发现有些服务无法从根 ServiceProvider 创建,所以为了让它工作,我们需要创建一个特定的作用域来检索它们。

需要牢记的事项

优点

  • 我们可以引入任意数量的自定义逻辑,使其可配置,并根据传入的请求做出决定,这包括 cookie、标头以及任何可能到达的内容。
  • 这不会影响应用程序的其余部分,从 HttpContext 的角度来看,或者任何控制器,用户和其他用户一样有效且经过身份验证。

缺点

由于这不像正常的身份验证那样基于 cookie,因此您需要确保您正在进行的检查非常快速且非常具体,就像在我们的案例中一样,一旦 SignalR 协商完成,如果它最终使用 WebSockets,它不太可能再次运行(我猜的,我可能错了)。

结论

我知道这在某种程度上是一种“hacky”的方法,尽管我可以肯定地说,它确实有效,尤其是在我们面临的场景中。

ASP.NET Core 的优点之一是您可以定义自己的管道,那么为什么不利用它,就像我所做的那样,制作一些有趣的中间件,介入并尝试系统的运作方式,使其适合您的需求。这不一定总是最好的方法,但我看到越来越多的人利用中间件系统提供的功能。

谢谢,祝您编码愉快。

© . All rights reserved.