使用 AddAzureKeyVault 使我的应用程序慢 10 秒

PEK*_*PEK 6 azure .net-core azure-keyvault

我有这个非常简单的 .NET Core 应用程序:

    static void Main(string[] args)
    {
        var builder = new ConfigurationBuilder()
           .SetBasePath(Directory.GetCurrentDirectory())
           .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

        builder.AddAzureKeyVault("https://MyKeyVault.vault.azure.net");

        var stopwatch = new Stopwatch();
        stopwatch.Start(); 
        var configuration = builder.Build();
        var elapsed = stopwatch.Elapsed;

        Console.WriteLine($"Elapsed time: {elapsed.TotalSeconds}");
    }
Run Code Online (Sandbox Code Playgroud)

csproj 文件如下所示:

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
  <PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" />
  <PackageReference Include="Microsoft.Extensions.Configuration.AzureKeyVault" Version="2.1.1" />
  <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.1.1" />
  <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.1.1" />
  <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" />
</ItemGroup>

</Project>
Run Code Online (Sandbox Code Playgroud)

我的问题是应用程序需要大约 10 秒才能在附加调试器的情况下执行(在没有调试器的情况下大约需要 5 秒)。如果我使用AddAzureKeyVault删除该行,应用程序将在不到一秒的时间内执行。我知道AddAzureKeyVault将使应用程序连接到 Azure 并从密钥保管库读取值,但我希望这会快得多。

这是预期的行为吗?有什么我可以做的更快吗?

jsc*_*ter 5

对于Microsoft.Azure.Services.AppAuthentication库,请参阅原始答案。有关更新的Azure.Identity库,请参阅更新 2021-03-22。


原答案:

是的,将AzureServiceTokenProvider显式配置为使用az cli进行身份验证。您可以通过设置名为AzureServicesAuthConnectionString.

重击:

export AzureServicesAuthConnectionString="RunAs=Developer; DeveloperTool=AzureCli"
Run Code Online (Sandbox Code Playgroud)

电源外壳:

$Env:AzureServicesAuthConnectionString = "RunAs=Developer; DeveloperTool=AzureCli"
Run Code Online (Sandbox Code Playgroud)

请注意,需要在您运行应用程序的任何会话中设置环境变量。

解释

问题的根源在关于身份验证的 MS 文档中有所暗示,其中指出,“默认情况下,AzureServiceTokenProvider使用多种方法来检索令牌。”

在使用的多种方法中,az cli身份验证是其中的一种。因此,AzureServiceTokenProvider在最终将az cli用作令牌源之前,需要一些时间来尝试在啄食顺序中更高的其他身份验证方法。在环境变量中设置连接字符串可以消除您等待其他身份验证方法失败的时间。

该解决方案是最好一个硬编码clientIdclientSecret不仅为了方便,也因为az cli身份验证并不需要你的硬编码clientSecret或将其存储在明文。


更新 (2021-03-22)

与较新的 Azure 客户端 SDK(如Azure.Security.KeyVault.Secrets)兼容的 Azure.Identity 身份验证提供程序具有基于代码的选项(而不是连接字符串)以跳过某些身份验证方法。你可以:

  1. DefaultAzureCredential 构造函数中设置排除项,或

  2. 使用更具体的类类型构造函数生成 TokenCredential (另请参阅此处的身份验证提供程序迁移图表)。

  • 不错的伎俩。要使用此功能,您需要首先在终端中运行命令 **az login**,或者如果您使用的是 Visual Studio,请使用连接字符串 **RunAs=Developer; DeveloperTool = VisualStudio 相反**。您还可以在 **launchSettings.json** 中设置环境变量。综上所述,我发现 clientid 解决方案更快。 (2认同)

PEK*_*PEK 1

之前建议的 clientId 和 AzureServiceTokenProvider 解决方案确实对已弃用的数据包Microsoft.Azure.KeyVault产生影响。但使用新数据包Azure.Security.KeyVault.Secrets在我的测量中不再需要这些解决方案。

我的解决方案是缓存 Azure KeyVault 中的配置并在本地存储该配置。通过此解决方案,您将能够在开发过程中使用 Azure KeyVault,并且仍然具有出色的性能。以下代码展示了如何执行此操作:

using Azure.Extensions.AspNetCore.Configuration.Secrets;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;

namespace ConfigurationCache
{
    public class Program
    {
        private static readonly Stopwatch Stopwatch = new Stopwatch();

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

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((ctx, builder) =>
                {
                    builder.AddAzureConfigurationServices();
                })
                .ConfigureServices((hostContext, services) =>
                {
                    Stopwatch.Stop();

                    Console.WriteLine($"Start time: {Stopwatch.Elapsed}");
                    Console.WriteLine($"Config: {hostContext.Configuration.GetSection("ConnectionStrings:MyContext").Value}");

                    services.AddHostedService<Worker>();
                });
    }

    public static class AzureExtensions
    {
        public static IConfigurationBuilder AddAzureConfigurationServices(this IConfigurationBuilder builder)
        {
            // Build current configuration. This is later used to get environment variables.
            IConfiguration config = builder.Build();

#if DEBUG
            if (Debugger.IsAttached)
            {
                // If the debugger is attached, we use cached configuration instead of
                // configurations from Azure.
                AddCachedConfiguration(builder, config);

                return builder;
            }
#endif

            // Add the standard configuration services
            return AddAzureConfigurationServicesInternal(builder, config);
        }

        private static IConfigurationBuilder AddAzureConfigurationServicesInternal(IConfigurationBuilder builder, IConfiguration currentConfig)
        {
            // Get keyvault endpoint. This is normally an environment variable.
            string keyVaultEndpoint = currentConfig["KEYVAULT_ENDPOINT"];

            // Setup keyvault services
            SecretClient secretClient = new SecretClient(new Uri(keyVaultEndpoint), new DefaultAzureCredential());
            builder.AddAzureKeyVault(secretClient, new AzureKeyVaultConfigurationOptions());

            return builder;
        }

        private static void AddCachedConfiguration(IConfigurationBuilder builder, IConfiguration currentConfig)
        {
            //Setup full path to cached configuration file.
            string path = System.IO.Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                "MyApplication");
            string filename = System.IO.Path.Combine(path, $"configcache.dat");

            // If the file does not exists, or is more than 12 hours, update the cached configuration.
            if (!System.IO.File.Exists(filename) || System.IO.File.GetLastAccessTimeUtc(filename).AddHours(12) < DateTime.UtcNow)
            {
                System.IO.Directory.CreateDirectory(path);

                UpdateCacheConfiguration(filename, currentConfig);
            }

            // Read the file
            string encryptedFile = System.IO.File.ReadAllText(filename);

            // Decrypt the content
            string jsonString = Decrypt(encryptedFile);

            // Create key-value pairs
            var keyVaultPairs = JsonSerializer.Deserialize<Dictionary<string, string>>(jsonString);

            // Use the key-value pairs as configuration
            builder.AddInMemoryCollection(keyVaultPairs);
        }

        private static void UpdateCacheConfiguration(string filename, IConfiguration currentConfig)
        {
            // Create a configuration builder. We will just use this to get the
            // configuration from Azure.
            ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();

            // Add the services we want to use.
            AddAzureConfigurationServicesInternal(configurationBuilder, currentConfig);

            // Build the configuration
            IConfigurationRoot azureConfig = configurationBuilder.Build();

            // Serialize the configuration to a JSON-string.
            string jsonString = JsonSerializer.Serialize(
                azureConfig.AsEnumerable().ToDictionary(a => a.Key, a => a.Value),
                options: new JsonSerializerOptions()
                {
                    WriteIndented = true
                }
                );

            //Encrypt the string
            string encryptedString = Encrypt(jsonString);

            // Save the encrypted string.
            System.IO.File.WriteAllText(filename, encryptedString);
        }

        // Replace the following with your favorite encryption code.

        private static string Encrypt(string str)
        {
            return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(str));
        }

        private static string Decrypt(string str)
        {
            return System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(str));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)