在 Blazor 服务器中的自定义委托处理程序中获取范围服务

Dum*_*DED 10 c# dependency-injection blazor-server-side .net-5

我正在尝试设置我们的 Blazor 服务器应用程序以使用自定义委托处理程序,该处理程序会将不记名令牌附加到我们 API 的所有传出请求。委托处理程序使用令牌服务来处理令牌的检索以及令牌过期时的刷新过程。代码如下所示:

\n
public class HttpAuthorizationHandler : DelegatingHandler\n{\n    private ITokenService _tokenService { get; set; }\n\n    public HttpAuthorizationHandler(ITokenService tokenService)\n    {\n        _tokenService = tokenService;\n    }\n\n    protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n    {\n        var token = await _tokenService.TryGetToken();\n\n        request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", token);\n\n        return await base.SendAsync(request, cancellationToken);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

我已将令牌服务注册为 Startup.cs 中的范围服务,并了解该服务的同一实例将在请求的生命周期内在 DI 容器中保持活动状态。如果我理解正确,这意味着我可以将令牌值分配给 App.razor 页面中的服务,并通过对令牌服务的任何后续调用来获取它们:

\n
@code {\n  [Parameter]\n  public string AccessToken { get; set; }\n  [Parameter]\n  public string RefreshToken { get; set; }\n\n  [Inject] private ITokenService _tokenService { get; set; }\n\n  protected override void OnInitialized()\n  {\n    _tokenService.AccessToken = AccessToken.NullIfEmpty() ?? _tokenService.AccessToken;\n    _tokenService.RefreshToken = RefreshToken.NullIfEmpty() ?? _tokenService.RefreshToken;\n  }\n
Run Code Online (Sandbox Code Playgroud)\n

这似乎对除委托处理程序之外的所有内容都适用- 对于对令牌服务发出的任何其他请求,这些值在作用域配置中显示得很好,但是当将令牌服务注入委托处理程序时,令牌值始终为 null 。

\n

授权处理程序中的访问令牌为空

\n

毫不奇怪,如果我将服务注册为单例,这些值会很好地传播到委托处理程序,但这显然不是一个解决方案。由于某种原因,处理程序的 DI 范围似乎与应用程序其余部分的不同,我不知道为什么。有任何想法吗?提前感谢您的帮助。

\n

编辑

\n

在 Simply Ged 和 Andrew Lock 的帮助下,我能够更接近地使用 IHttpContextAccessor 来获取与请求关联的 ITokenService 实例。当我第一次登录并看到它工作时,我欣喜若狂,但当我的兴奋消退后,我发现它会在短时间内停止正常工作。事实证明,此方法仅同步初始请求上的实例 - 之后,DI 笨拙的管道管理就会启动,并为每个后续请求维护相同的服务实例,并且它们再次失去对齐。这是我测试的调试日志的示例:

\n
App.razor:         e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x90\nAuthStateProvider: e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x9c Initial request - all IDs match\nAuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x98\nAuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x90\nAuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x82\nAuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x82\nAuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x9c Clicking around a bunch\nAuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x82\nAuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x82\nAuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x98\nApp.razor:         cab70e54-1907-462d-8918-dbd771fabe76 \xe2\x94\x90\nAuthStateProvider: cab70e54-1907-462d-8918-dbd771fabe76 \xe2\x94\x9c Load a new page - ah, crap...\nAuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x98\nAuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x90\nAuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x82\nApp.razor:         3c55ff9c-5511-40a1-9fb8-cd00f9fc11c6 \xe2\x94\x82\nAuthStateProvider: 3c55ff9c-5511-40a1-9fb8-cd00f9fc11c6 \xe2\x94\x9c Dang it all to heck\nAuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x82\nAuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x82\nAuthHandler:       e12f6c80-5cee-44a0-a8a0-d6b783057339 \xe2\x94\x98\n
Run Code Online (Sandbox Code Playgroud)\n

Andrew Lock 关于这个主题的优秀博客文章详细介绍了为什么会发生这种事情 - 显然依赖注入管理 http 请求管道的生命周期与 http 客户端本身的生命周期分开,所以你可以不依赖于管道是与请求相同的上下文的一部分。他建议使用 IHttpContextAccessor 作为此问题的解决方案,但似乎并不能解决此特定情况下的问题。我怀疑他的博客文章指的是 ASP.NET Core 的早期版本,其行为不适用于 Blazor 应用程序。事实上,正如 Alex 指出的那样,微软特别建议不要使用 IHttpContextAccessor 来实现共享状态

\n

Dum*_*DED 6

经过几天的故障排除后,我不得不承认委托处理程序根本不是一个可行的解决方案。由于依赖注入完全独立于 http 客户端本身来管理 http 请求管道范围,因此没有可靠的方法来确保管道与请求具有相同的范围,甚至破坏协议并使用 IHttpContextAccessor 也无法解决问题。

因此,我完全放弃了委托处理程序,而是为所有要继承的服务实现了一个基类,该基类在构造函数中处理令牌获取。这样我就可以将令牌服务直接注入到构造函数中,并确保我获得正确的范围。

public class ServiceBase
{
    public readonly HttpClient _HttpClient;

    public ServiceBase(HttpClient httpClient, ITokenService tokenService)
    {
        _HttpClient = httpClient;

        var token = tokenService.TryGetToken();

        if (!string.IsNullOrEmpty(token))
        {
            _HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        }
        else
        {
            _HttpClient.DefaultRequestHeaders.Authorization = null;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它不像使用 DI 管道那么漂亮,但它至少是干净和有效的,并且到目前为止它运行得很好。正如一位智者(某种程度上)曾经说过的那样,“除了[使用依赖注入]之外,还有其他选择。”