You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET 7 Web API指纹认证加载Azure Key Vault密钥失败

.NET 7 Web API通过指纹认证从Azure Key Vault加载密钥失败问题

我在.NET 7 Web API应用中尝试通过指纹认证从Azure Key Vault加载密钥值,已实现相关配置代码,但调试时ServiceBusConfigOption.ConnectionString的值仍显示为@Microsoft.KeyVault(SecretUri={SecretURL}/),未加载实际密钥值。相关代码如下:

配置扩展方法

public static class ConfigurationExtensions
{
    public static IConfigurationBuilder AddAppConfiguration(this IConfigurationBuilder configurationBuilder, IWebHostEnvironment hostingEnvironment)
    {
        configurationBuilder
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);

        // For Localhost, we're using appsettings.json file, it will not be overridden when running on Localhost
        if (!hostingEnvironment.IsEnvironment("Localhost"))
        {
            configurationBuilder.AddJsonFile($"appsettings.{hostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: true);
        }
        configurationBuilder.AddEnvironmentVariables();

        return configurationBuilder;
    }

    public static IConfigurationBuilder AddAzureKeyVaultConfiguration(this IConfigurationBuilder configurationBuilder, IWebHostEnvironment hostingEnvironment)
    {
        if (!hostingEnvironment.IsEnvironment("Development"))
        {
            var builtConfig = configurationBuilder.Build();
            var keyVaultEndpoint = $"https://{builtConfig["KeyVaultConfigOption:Name"]}.vault.azure.net/";

            var clientId = builtConfig["KeyVaultConfigOption:ClientId"];
            var thumbprint = builtConfig["KeyVaultConfigOption:Thumbprint"];
            var tenantId = builtConfig["KeyVaultConfigOption:TenantId"];

            var certificate = GetCertificate(thumbprint);

            var clientCertificateCredential = new ClientCertificateCredential(tenantId, clientId, certificate);

            configurationBuilder.AddAzureKeyVault(new Uri(keyVaultEndpoint), clientCertificateCredential);
        }
        
        return configurationBuilder;
    }
    
    private static X509Certificate2 GetCertificate(string thumbprint)
    {
        var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);

        store.Open(OpenFlags.ReadOnly);

        var cert = store.Certificates.OfType<X509Certificate2>()
            .FirstOrDefault(x => x.Thumbprint == thumbprint);
        store.Close();

        if (cert == null)
            throw new InvalidOperationException($"Failed to find the certificate for thumbprint:{thumbprint}");

        return cert;
    }
}

Program.cs

var builder = WebApplication.CreateBuilder(args);
{
    // Environment configuration
    var configuration = builder.Configuration;
    var env = builder.Environment;

    configuration.AddAppConfiguration(env);

    // Add azure key vault configuration
    builder.Configuration.AddAzureKeyVaultConfiguration(env);

    builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<Program>());
    builder.Services.AddHttpClient();

    builder.Services.AddApplication();
    builder.Services.AddInfrastructure(builder.Configuration);

    builder.Services.AddHelpers();

    builder.Services.AddControllers()
       // Configures the JSON serialization options for controllers.
       .AddJsonOptions(options =>
        {
            options.JsonSerializerOptions.PropertyNamingPolicy = null;
        });

    builder.Services.Configure<ApiBehaviorOptions>(options =>
    {
        options.SuppressModelStateInvalidFilter = true;
    });
}

var app = builder.Build();
{
    app.UseHttpsRedirection();

    // Authentication & Authorization
    app.UseAuthentication();
    app.UseAuthorization();
    app.MapControllers();

    app.UseRouting();
    app.UseSwaggerWithVersioning();
}

if (app.Environment.IsDevelopment())
{
    //DEV configurations
}

app.Run();

appsettings.json

"ServiceBusConfigOption": {
    "ConnectionString": "@Microsoft.KeyVault(SecretUri={SecretURL}/)",
    "TopicsEmailTemplate": "EmailTemplates",
    "TopicUserSetting": "UserSettings"
  }

注入代码

public class AzureServiceBusFactory : IMessageBusFactory
{
    private readonly ServiceBusConfigOption _serviceBusConfigOption;

    public AzureServiceBusFactory(IOptions<ServiceBusConfigOption> configurationSettings)
    {
       _serviceBusConfigOption = configurationSettings.Value;
    }
}

问题原因与解决方案

核心问题

当前使用的AddAzureKeyVault方法仅会将Key Vault中的密钥直接加载到配置根节点,不会自动解析配置文件中@Microsoft.KeyVault(...)格式的引用。这种引用格式是Azure App Service/Azure App Configuration的特有功能,原生.NET配置系统不支持自动解析。

解决方案步骤

1. 启用Key Vault密钥引用解析

修改AddAzureKeyVaultConfiguration方法,使用SecretManagerOptions配置密钥加载器,实现引用解析:

public static IConfigurationBuilder AddAzureKeyVaultConfiguration(this IConfigurationBuilder configurationBuilder, IWebHostEnvironment hostingEnvironment)
{
    if (!hostingEnvironment.IsEnvironment("Development"))
    {
        var builtConfig = configurationBuilder.Build();
        var keyVaultEndpoint = $"https://{builtConfig["KeyVaultConfigOption:Name"]}.vault.azure.net/";

        var clientId = builtConfig["KeyVaultConfigOption:ClientId"];
        var thumbprint = builtConfig["KeyVaultConfigOption:Thumbprint"];
        var tenantId = builtConfig["KeyVaultConfigOption:TenantId"];

        var certificate = GetCertificate(thumbprint);
        var credential = new ClientCertificateCredential(tenantId, clientId, certificate);
        var secretClient = new SecretClient(new Uri(keyVaultEndpoint), credential);

        // 配置密钥管理器,启用引用解析
        configurationBuilder.AddAzureKeyVault(
            secretClient,
            new SecretManagerOptions
            {
                // 自动解析@Microsoft.KeyVault格式的引用
                SecretLoader = new SecretClientSecretLoader(secretClient),
                ReloadInterval = TimeSpan.FromMinutes(5)
            });
    }
    
    return configurationBuilder;
}

2. 调整配置加载顺序与构建逻辑

确保Key Vault配置加载后重新构建配置,让解析后的引用生效:

var builder = WebApplication.CreateBuilder(args);
{
    var env = builder.Environment;

    // 加载基础配置文件
    builder.Configuration.AddAppConfiguration(env);

    // 添加Key Vault配置
    builder.Configuration.AddAzureKeyVaultConfiguration(env);

    // 重新构建配置,应用Key Vault的解析结果
    var resolvedConfig = builder.Configuration.Build();

    // 使用解析后的配置注册服务
    builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<Program>());
    builder.Services.AddHttpClient();

    builder.Services.AddApplication();
    builder.Services.AddInfrastructure(resolvedConfig);

    // ... 其他服务注册代码
}

3. 验证基础配置正确性

  • 确认证书已安装在目标环境的CurrentUser\My证书存储中,指纹为大写无空格字符串,与配置一致。
  • 检查Azure AD应用(对应ClientId)拥有Key Vault的Secret Get权限,且已在Key Vault访问策略中配置。
  • 确保{SecretURL}是Key Vault密钥的完整URI(例如https://yourvault.vault.azure.net/secrets/ServiceBusConnectionString/abc123)。

4. 替代方案:手动解析配置值

若不想依赖扩展包,可在读取配置时手动替换引用:

public class AzureServiceBusFactory : IMessageBusFactory
{
    private readonly ServiceBusConfigOption _serviceBusConfigOption;

    public AzureServiceBusFactory(IOptions<ServiceBusConfigOption> configurationSettings, IConfiguration configuration)
    {
        _serviceBusConfigOption = configurationSettings.Value;
        var connectionString = _serviceBusConfigOption.ConnectionString;

        // 手动解析Key Vault引用格式
        if (connectionString.StartsWith("@Microsoft.KeyVault(SecretUri="))
        {
            var secretUri = connectionString.Replace("@Microsoft.KeyVault(SecretUri=", "").TrimEnd(')');
            // 从配置中获取Key Vault加载的密钥值(密钥名称为URI最后一段)
            var secretName = secretUri.Split('/').Last();
            _serviceBusConfigOption.ConnectionString = configuration[secretName];
        }
    }
}

关键注意事项

  • 原生.NET配置系统不支持自动解析@Microsoft.KeyVault格式,需通过扩展包或手动代码实现。
  • 若使用Key Vault直接加载密钥,可简化appsettings.json配置为"ConnectionString": "ServiceBusConnectionString",确保Key Vault中存在同名密钥即可。

内容的提问来源于stack exchange,提问作者Rakesh Kumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 01:22:59