ASP.NET Core 通过 HTTP 接收自定义配置来配置 Kestrel

con*_*l_c 3 c# kestrel-http-server asp.net-core asp.net-core-webapi

这感觉像是一个需要解决的简单问题,但在我的所有搜索中,我仍然没有找到适合我的解决方案。可能是另一种找不到我想要的东西的情况,因为我没有寻找正确的“东西”,但我们在这里......

我有一个 C# Web API 程序,我想从配置对象配置 kestrel 服务器。

我通过休息调用将此配置接收到我的服务中,进入一个CustomConfig对象。我可以在 inProgram.cs或 in 中获取此配置对象Startup.cs,但由于我不想重复自己并进行额外的调用,所以我不想在这两个地方都这样做。

我的偏好是获取配置,Startup.cs因为这是我的其余配置代码所在的位置,并且是我已经使用我的CustomConfig对象的位置。但是,我找不到一种方法来配置 kestrel 服务器以使用我提供给它的证书(在 Startup.cs 中),也找不到一种将此配置注入到Startup.csfrom的方法Program.cs

在其他项目中,我已将 PFX 文件的位置作为环境变量传递:(ASPNETCORE_Kestrel__Certificates__Default__Path在这种情况下,一切都可以正常工作,无需额外的代码配置),但在这个项目中,所有配置都必须通过其余调用检索,因此这里不是一个选项。

我目前一切都在运行,但只能通过两次其余调用来获取配置。配置 kestrel 的当前实现是将 PFX 存储CustomConfig为 base64 字符串,并在以下位置进行配置Program.cs

public static IHostBuilder CreateHostBuilder(string[] args)
        {
            return Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    CustomConfig config = CustomConfig() // <- I receive config here
                    webBuilder.UseStartup<Startup>();
                    webBuilder.UseKestrel(options =>
                    {
                       options.ConfigureHttpsDefaults(d =>
                       {
                           byte[] pfxBytes = Convert.FromBase64String(config.Base64PFXBytes);
                           d.ServerCertificate = new X509Certificate2(pfxBytes, "sslKey");
                       });
                    });
                });
        }
Run Code Online (Sandbox Code Playgroud)

总结一下..

  • 我有一个CustomConfig用于配置服务的对象Startup.cs
  • 我想从我的服务器配置我的 Kestrel 服务器CustomConfig

所以我正在寻求帮助:

  • 让 Kestrel 使用我的 PFXStartup.cs
  • CustomConfig对象从Program.cs传入Startup.cs

希望这是有道理的.. 欢迎任何和所有解决方案/其他问题以澄清!

提前致谢!

abd*_*sco 6

ASP.NET Core 使用接口抽象配置IConfiguration。无需详细说明,它会从各个来源收集配置IConfigurationSource并将它们相互叠加,这使我们可以通过使用相同的键定义一个源中定义的设置来覆盖另一个源中定义的设置。

1. 实施IConfigurationSource

让我们实现一个IConfigurationSource. 我们可以使用ConfigurationSource抽象类作为我们的起点。我们将使用内存中实现,然后切换到远程源。

class RemoteConfigurationSource : IConfigurationSource
{
    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new RemoteConfigurationProvider(_options);
    }

    private class RemoteConfigurationProvider : ConfigurationProvider
    {
        public override void Load()
        {
            // TODO: fetch data from the API
            var remoteConfig = new Dictionary<string, string>
            {
                { "CertificateOptions:PfxBase64", "MIIKkQIBAz....gfQ" },
                { "CertificateOptions:Password", "secret" },
            };
            Data = remoteConfig;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后将其添加到回调中的配置生成器中ConfigureHostConfiguration

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureHostConfiguration(builder =>
            {
                // add new source
                builder.AddRemoteConfiguration();
            })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>().UseKestrel((context, options) =>
                {
                    var certificateOptions = context.Configuration
                        .GetSection(KestrelCertificateOptions.ConfigurationKey)
                        .Get<KestrelCertificateOptions>();
                    options.ConfigureHttpsDefaults(adapterOptions =>
                        adapterOptions.ServerCertificate = certificateOptions.Certificate);
                });
            });
}

public static class ConfigurationBuilderExtensions
{
    public static IConfigurationBuilder AddRemoteConfiguration(this IConfigurationBuilder builder) =>
        builder.Add(new RemoteConfigurationSource());
}

class KestrelCertificateOptions
{
    public const string ConfigurationKey = "CertificateOptions";
    public string PfxBase64 { get; set; }
    public string Password { get; set; }
    public X509Certificate2 Certificate => new X509Certificate2(Convert.FromBase64String(PfxBase64), Password);
}
Run Code Online (Sandbox Code Playgroud)

当我们运行应用程序时,ASP.NET Core 将加载并使用我们的内存中配置。

2. 从 API 获取配置数据

现在让我们从远程 API 获取配置。它需要返回配置值,其部分名称用冒号分隔:。这是与 JSON 相同的配置,在以下CertificateOptions部分下归档:

{
  "CertificateOptions:PfxBase64": "MII....oCAgfQ",
  "CertificateOptions:Password": "secret"
}
Run Code Online (Sandbox Code Playgroud)

假设 API 包装返回的数据包装为:

{
  "Application": "MyApp",
  "LastChanged": "2021-08-09 14:38:00",
  "Data": {
    "CertificateOptions:PfxBase64": "MIIK...oCAgfQ",
    "CertificateOptions:Password": "secret"
  }
}
Run Code Online (Sandbox Code Playgroud)

Data所以我们在获取数据时只需要考虑key。

class RemoteConfigurationSource : IConfigurationSource
{
    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new RemoteConfigurationProvider();
    }

    private class RemoteConfigurationProvider : ConfigurationProvider
    {
        public override void Load()
        {
            // We cannot await this method, so have to do sync-over-async. 
            // Not an issue, because it's a one-time thing.
            var result = LoadRemoteConfig().GetAwaiter().GetResult();
            Data = result.Data;
        }

        private async Task<RemoteConfigResult> LoadRemoteConfig()
        {
            // We cannot use IHttpClientFactory here, since ServiceProvider isn't even built yet.
            using var httpClient = new HttpClient();
            // ... add headers, token to request
            return await httpClient.GetFromJsonAsync<RemoteConfigResult>("https://example.com/path/to/json");
        }
    }

    private class RemoteConfigResult
    {
        public Dictionary<string, string> Data { get; set; }
    }
}
Run Code Online (Sandbox Code Playgroud)

为了稍微清理一下,我们可以将 URL 和其他凭据移至 appsettings.json:

{
  "Logging": {
    /*...*/
  },
  "RemoteConfiguration": {
    "Url": "https://jsonkeeper.com/b/B78I",
    "ApplicationId": "myconfigappid",
    "Secret": "myconfigapisecret"
  }
}
Run Code Online (Sandbox Code Playgroud)

然后构建一个临时IConfiguration添加所需数量的源,然后获取这些值:

// Read credentials from appsettings.json
var remoteConfigurationOptions = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json", optional: false)
    .Build()
    .GetSection(RemoteConfigurationOptions.ConfigurationKey)
    .Get<RemoteConfigurationOptions>();
    
public class RemoteConfigurationOptions
{
    public const string ConfigurationKey = "RemoteConfiguration";
    public string Url { get; set; }
    public string ApplicationId { get; set; }
    public string Secret { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

然后将此对象传递给我们的配置源,配置源又将其传递给配置提供者