从依赖注入配置进行依赖调用

Posted

技术标签:

【中文标题】从依赖注入配置进行依赖调用【英文标题】:Make dependency calls from dependency injection configuration 【发布时间】:2021-10-14 09:03:41 【问题描述】:

我的需要是立即注入一个 HttpClient 并准备好使用。但需要注意的是 HttpClient 需要设置 Authorization 标头,为此我需要再进行一次调用,获取令牌。 我设法在初创公司的 RegisterServices 中完成了所有这些配置,但我怀疑这是否是个好主意。

services.AddHttpClient("OidcClient", (isp, client) =>

    var options = isp.GetRequiredService<IOptions<MyConfig>>().Value;
    client.BaseAddress = new Uri(options.OidcUrl);
);

services.AddHttpClient("MyClient", (isp, client) =>

    var options = isp.GetRequiredService<IOptions<MyConfig>>().Value;
    var oidcClient = isp.GetRequiredService<IHttpClientFactory>().CreateClient("OidcClient");
    var data = new Dictionary<string, string>
    
        "client_id", options.ClientId,
        "client_secret", options.ClientSecret
    ;

    var request = new HttpRequestMessage(HttpMethod.Post, "/connect/token")  Content = new FormUrlEncodedContent(data) ;
    var response = oidcClient.SendAsync(request).Result;

    var token = response.Content.ReadFromJsonAsync<TokenResponse>().Result;

    client.BaseAddress = new Uri(options.Url);
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
);

然后它被正确注入到我的代码中,我可以使用带有授权标头的客户端。

所以,我担心的是:

在依赖配置中进行 HTTP 调用是否正常? 没有找到比 .Result 更好的异步调用方法,有替代方法吗?

还有一个主要问题:在 DI 配置中进行依赖调用是一个“好主意”吗?

【问题讨论】:

使用 deliging 处理程序,这是它们设计的用例之一,它们是异步友好的 哇,DelegatingHandler 太棒了,完全符合我的需要。感谢您的提示。 【参考方案1】:

在依赖配置中进行 HTTP 调用是否正常[/一个“好主意”]?

不,这与启动和运行您的依赖项所需的最低要求相去甚远,它包含属于您的 API 客户端类的特定于实现的行为。

您也不知道应用程序将运行多长时间以及令牌将在多长时间内有效,因此期望在其构造函数中获得有效且随时可用的 HttpClient 的代码可能会一直保留它,只要它愿望。

将您的 HttpClient 包装在第三方或您自己的包含 OAuth 流程的包装器中。当您的代码想要首次调用 API、令牌过期或使用现有令牌获得 401 时,进行身份验证调用。

正如@TheGeneral 所评论的,这种包装器的一种方法是使用DelegatingHandler,例如this blog post 中的说明。总结:

设置:

// The DelegatingHandler has to be registered as a Transient Service
services.AddTransient<ProtectedApiBearerTokenHandler>();

// Register our ProtectedApi client with a DelegatingHandler
// that knows how to obtain an access_token
services.AddHttpClient<IProtectedApiClient, ProtectedApiClient>(client =>

    client.BaseAddress = new Uri("http://localhost:5002");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
).AddHttpMessageHandler<ProtectedApiBearerTokenHandler>();

处理程序:

public class ProtectedApiBearerTokenHandler : DelegatingHandler

    private readonly IIdentityServerClient _identityServerClient;

    public ProtectedApiBearerTokenHandler(
        IIdentityServerClient identityServerClient)
    
        _identityServerClient = identityServerClient 
            ?? throw new ArgumentNullException(nameof(identityServerClient));
    

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, 
        CancellationToken cancellationToken)
    
        // request the access token
        var accessToken = await _identityServerClient.RequestClientCredentialsTokenAsync();

        // set the bearer token to the outgoing request
        request.SetBearerToken(accessToken);

        // Proceed calling the inner handler, that will actually send the request
        // to our protected api
        return await base.SendAsync(request, cancellationToken);
    

【讨论】:

【参考方案2】:

虽然 CodeCasters 的回答非常正确。理想情况下,您会为此任务使用委托处理程序。

它允许您在各个阶段拦截请求并在请求中添加所需的任何人工制品,在您的情况下授权属性,它也是异步友好的。

但是要注意错误以及返回给调用者的内容。以免给消费者带来惊喜

【讨论】:

【参考方案3】:

虽然@CodeCaster 帮助我理解令牌获取不应该成为配置的一部分,但@TheGeneral 暗示使用 DelegatingHandler,这在我看来是解决此问题的理想解决方案。 最后,我在配置中来到了这段代码:

services.AddTransient<AuthorizationHandler>();
services.AddHttpClient<AuthorizationHandler>((isp, client) =>

    var options = isp.GetRequiredService<IOptions<MyConfig>>().Value;
    client.BaseAddress = new Uri(options.OidcUrl);
);
services.AddHttpClient("MyClient", (isp, client) =>

    var options = isp.GetRequiredService<IOptions<MyConfig>>().Value;
    client.BaseAddress = new Uri(options.Url);
).AddHttpMessageHandler<AuthorizationHandler>();

还有一个处理程序本身:

public class AuthorizationHandler : DelegatingHandler

    private readonly HttpClient _client;
    private readonly MyConfig _options;

    public AuthorizationHandler(HttpClient client, IOptions<MyConfig> options)
    
        _client = client;
        _options = options.Value;
    

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    
        var token = await GetToken();

        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        return await base.SendAsync(request, cancellationToken);
    
    ...

【讨论】:

以上是关于从依赖注入配置进行依赖调用的主要内容,如果未能解决你的问题,请参考以下文章

IOC容器

PHP 依赖注入和松耦合

Spring 依赖注入原理

依赖注入总结

Spring使用注解配置依赖注入

Spring依赖注入