安全存储中 ASP.NET Core 中开发的应用程序机密Safe storage of app secrets in development in ASP.NET Core

本文内容

作者: Rick AndersonDaniel RothScott Addie

查看或下载示例代码如何下载

本文档介绍在开发计算机上开发 ASP.NET Core 应用过程中存储和检索敏感数据的方法。永远不要将密码或其他敏感数据存储在源代码中。不应使用生产机密进行开发或测试。机密不应与应用一起部署。相反,机密应通过受控方式(如环境变量、Azure Key Vault 等)在生产环境中可用。可以通过Azure Key Vault 配置提供程序存储和保护 Azure 测试和生产机密。

环境变量Environment variables

使用环境变量以避免在代码中或在本地配置文件中的应用机密的存储。环境变量重写所有以前指定的配置源的配置的值。

通过在 Startup 构造函数中调用 AddEnvironmentVariables 来配置读取环境变量值:

  1. public Startup(IHostingEnvironment env)
  2. {
  3. var builder = new ConfigurationBuilder()
  4. .SetBasePath(env.ContentRootPath)
  5. .AddJsonFile("appsettings.json",
  6. optional: false,
  7. reloadOnChange: true)
  8. .AddEnvironmentVariables();
  9. if (env.IsDevelopment())
  10. {
  11. builder.AddUserSecrets<Startup>();
  12. }
  13. Configuration = builder.Build();
  14. }

请考虑一个 ASP.NET Core web 应用,其中启用了单个用户帐户安全。具有 DefaultConnection密钥的项目的appsettings文件中包含默认的数据库连接字符串。默认连接字符串是 localdb,这在用户模式下运行,不需要密码。在应用程序部署过程中,可以使用环境变量的值覆盖 DefaultConnection 键值。环境变量可以存储敏感凭据与完整的连接字符串。

警告

环境变量通常存储在普通的未加密的文本。如果计算机或进程受到攻击,可以由不受信任方访问环境变量。可能需要更多措施,防止用户机密泄露。

所有平台上的环境变量分层键都不支持 : 分隔符。__(双下划线):

  • 受所有平台支持。例如,Bash 不支持 : 分隔符,但支持 __
  • 自动替换为 :

机密管理器Secret Manager

密码管理器工具在 ASP.NET Core 项目的开发过程中存储敏感数据。在此上下文中,一种敏感数据是应用程序密码。应用程序机密存储在项目树不同的位置。与特定项目关联或在多个项目之间共享的应用程序机密。应用机密不会签入源代码管理。

警告

机密管理器工具不会加密存储的机密,不应被视为受信任存储区。它是仅限开发目的。在用户配置文件目录中的 JSON 配置文件中存储的键和值。

机密管理器工具的工作原理How the Secret Manager tool works

机密管理器工具抽象化实现详细信息,例如位置和方式存储的值。如果不知道这些实现的详细信息,可以使用该工具。值存储在本地计算机上 JSON 配置文件中的系统保护的用户配置文件文件夹中:

文件系统路径:

%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json

文件系统路径:

~/.microsoft/usersecrets/<user_secrets_id>/secrets.json

在前面的文件路径中,将 <usersecrets_id> 替换为 .csproj_文件中指定的 UserSecretsId 值。

不编写代码,取决于使用机密管理器工具中保存的数据的格式的位置。这些实现细节可能会更改。例如,机密的值不会加密,但可能在将来。

安装机密管理器工具Install the Secret Manager tool

机密管理器工具是可与.NET Core CLI,在.NET Core SDK 2.1.300 捆绑或更高版本。有关.NET Core SDK 2.1.300 之前的版本中,工具安装是必需的。

提示

若要查看已安装的 .NET Core SDK 版本号,请从命令行界面运行 dotnet —version

如果正在使用的.NET Core SDK 包括的工具,将显示警告:

  1. The tool 'Microsoft.Extensions.SecretManager.Tools' is now included in the .NET Core SDK. Information on resolving this warning is available at (https://aka.ms/dotnetclitools-in-box).

在 ASP.NET Core 项目中安装SecretManager NuGet 包。例如:

  1. <Project Sdk="Microsoft.NET.Sdk.Web">
  2. <PropertyGroup>
  3. <TargetFramework>netcoreapp1.1</TargetFramework>
  4. <UserSecretsId>1242d6d6-9df3-4031-b031-d9b27d13c25a</UserSecretsId>
  5. </PropertyGroup>
  6. <ItemGroup>
  7. <PackageReference Include="Microsoft.AspNetCore"
  8. Version="1.1.6" />
  9. <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets"
  10. Version="1.1.2" />
  11. <PackageReference Include="System.Data.SqlClient"
  12. Version="4.5.0" />
  13. </ItemGroup>
  14. <ItemGroup>
  15. <DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools"
  16. Version="1.0.1" />
  17. </ItemGroup>
  18. </Project>

在验证工具的安装命令行界面中执行以下命令:

  1. dotnet user-secrets -h

机密管理器工具会显示示例用法、 选项和命令帮助:

  1. Usage: dotnet user-secrets [options] [command]
  2. Options:
  3. -?|-h|--help Show help information
  4. --version Show version information
  5. -v|--verbose Show verbose output
  6. -p|--project <PROJECT> Path to project. Defaults to searching the current directory.
  7. -c|--configuration <CONFIGURATION> The project configuration to use. Defaults to 'Debug'.
  8. --id The user secret ID to use.
  9. Commands:
  10. clear Deletes all the application secrets
  11. list Lists all the application secrets
  12. remove Removes the specified user secret
  13. set Sets the user secret to the specified value
  14. Use "dotnet user-secrets [command] --help" for more information about a command.

备注

您必须与 .csproj文件位于同一目录中,才能运行 .csproj文件的 DotNetCliToolReference 元素中定义的工具。

启用密钥存储Enable secret storage

机密管理器工具对存储在用户配置文件中的特定于项目的配置设置进行操作。

机密管理器工具在 .NET Core SDK 3.0.100 或更高版本中包含 init 命令。若要使用用户机密,请在项目目录中运行以下命令:

  1. dotnet user-secrets init

前面的命令将 UserSecretsId 元素添加到 .csproj文件的 PropertyGroup 中。默认情况下,UserSecretsId 的内部文本是一个 GUID。内部文本是任意的,但对项目是唯一的。

若要使用用户机密,请在 .csproj文件的 PropertyGroup 中定义 UserSecretsId 元素。UserSecretsId 的内部文本是任意的,但对项目是唯一的。开发人员通常会为 UserSecretsId生成 GUID。

  1. <PropertyGroup>
  2. <TargetFramework>netcoreapp2.1</TargetFramework>
  3. <UserSecretsId>79a3edd0-2092-40a2-a04d-dcb46d5ca9ed</UserSecretsId>
  4. </PropertyGroup>
  1. <PropertyGroup>
  2. <TargetFramework>netcoreapp1.1</TargetFramework>
  3. <UserSecretsId>1242d6d6-9df3-4031-b031-d9b27d13c25a</UserSecretsId>
  4. </PropertyGroup>

提示

在 Visual Studio 中,右键单击 "解决方案资源管理器中的项目,然后从上下文菜单中选择"管理用户机密"。此笔势将使用 GUID 填充的 UserSecretsId 元素添加到 .csproj文件。

设置的机密Set a secret

定义由一个键和其值组成的应用程序密码。该机密与项目的 UserSecretsId 值相关联。例如,从 .csproj文件所在的目录运行以下命令:

  1. dotnet user-secrets set "Movies:ServiceApiKey" "12345"

在前面的示例中,冒号表示 Movies 是具有 ServiceApiKey 属性的对象文本。

机密管理器工具可以过使用从其他目录。使用 —project 选项提供 .csproj文件所在的文件系统路径。例如:

  1. dotnet user-secrets set "Movies:ServiceApiKey" "12345" --project "C:\apps\WebApp1\src\WebApp1"

Visual Studio 中的 JSON 结构平展JSON structure flattening in Visual Studio

Visual Studio 的 "管理用户机密" 手势在文本编辑器中打开一个密码文件。私钥的内容替换为要存储的键/值对。例如:

  1. {
  2. "Movies": {
  3. "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  4. "ServiceApiKey": "12345"
  5. }
  6. }

JSON 结构在通过 dotnet user-secrets removedotnet user-secrets set进行修改后进行平展。例如,运行 dotnet user-secrets remove "Movies:ConnectionString" 会折叠 Movies 对象文本。修改后的文件如下所示:

  1. {
  2. "Movies:ServiceApiKey": "12345"
  3. }

设置多个机密Set multiple secrets

可以通过管道 JSON 将机密批处理设置为 set 命令。在下面的示例中,将输入 json文件的内容传送到 set 命令。

打开命令外壳中,并执行以下命令:

  1. type .\input.json | dotnet user-secrets set

打开命令外壳中,并执行以下命令:

  1. cat ./input.json | dotnet user-secrets set

访问机密Access a secret

ASP.NET Core 配置 API提供对机密管理器密码的访问权限。

如果你的项目以 .NET Framework 为目标,请安装UserSecrets NuGet 包。

在 ASP.NET Core 2.0 或更高版本中,当项目调用时,用户机密配置源会自动添加到 CreateDefaultBuilder 以使用预先配置的默认值初始化主机的新实例。DevelopmentEnvironmentName 时,CreateDefaultBuilder 调用 AddUserSecrets

  1. public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
  2. WebHost.CreateDefaultBuilder(args)
  3. .UseStartup<Startup>();
  1. public static IHostBuilder CreateHostBuilder(string[] args) =>
  2. Host.CreateDefaultBuilder(args)
  3. .ConfigureWebHostDefaults(webBuilder =>
  4. {
  5. webBuilder.UseStartup<Startup>();
  6. });

如果未调用 CreateDefaultBuilder,请通过在 Startup 构造函数中调用 AddUserSecrets 来显式添加用户机密配置源。仅在开发环境中运行应用时调用 AddUserSecrets,如以下示例中所示:

  1. public Startup(IHostingEnvironment env)
  2. {
  3. var builder = new ConfigurationBuilder()
  4. .SetBasePath(env.ContentRootPath)
  5. .AddJsonFile("appsettings.json",
  6. optional: false,
  7. reloadOnChange: true)
  8. .AddEnvironmentVariables();
  9. if (env.IsDevelopment())
  10. {
  11. builder.AddUserSecrets<Startup>();
  12. }
  13. Configuration = builder.Build();
  14. }
  1. public Startup(IWebHostEnvironment env)
  2. {
  3. var builder = new ConfigurationBuilder()
  4. .SetBasePath(env.ContentRootPath)
  5. .AddJsonFile("appsettings.json",
  6. optional: false,
  7. reloadOnChange: true)
  8. .AddEnvironmentVariables();
  9. if (env.IsDevelopment())
  10. {
  11. builder.AddUserSecrets<Startup>();
  12. }
  13. Configuration = builder.Build();
  14. }

安装 " UserSecrets " NuGet 包。

使用 Startup 构造函数中的 AddUserSecrets 调用添加用户机密配置源:

  1. public Startup(IHostingEnvironment env)
  2. {
  3. var builder = new ConfigurationBuilder()
  4. .SetBasePath(env.ContentRootPath)
  5. .AddJsonFile("appsettings.json",
  6. optional: false,
  7. reloadOnChange: true)
  8. .AddEnvironmentVariables();
  9. if (env.IsDevelopment())
  10. {
  11. builder.AddUserSecrets<Startup>();
  12. }
  13. Configuration = builder.Build();
  14. }

可以通过 Configuration API 检索用户机密:

  1. public class Startup
  2. {
  3. private string _moviesApiKey = null;
  4. public Startup(IConfiguration configuration)
  5. {
  6. Configuration = configuration;
  7. }
  8. public IConfiguration Configuration { get; }
  9. public void ConfigureServices(IServiceCollection services)
  10. {
  11. _moviesApiKey = Configuration["Movies:ServiceApiKey"];
  12. }
  13. public void Configure(IApplicationBuilder app)
  14. {
  15. app.Run(async (context) =>
  16. {
  17. var result = string.IsNullOrEmpty(_moviesApiKey) ? "Null" : "Not Null";
  18. await context.Response.WriteAsync($"Secret is {result}");
  19. });
  20. }
  21. }
  1. public class Startup
  2. {
  3. private string _moviesApiKey = null;
  4. public Startup(IHostingEnvironment env)
  5. {
  6. var builder = new ConfigurationBuilder()
  7. .SetBasePath(env.ContentRootPath)
  8. .AddJsonFile("appsettings.json",
  9. optional: false,
  10. reloadOnChange: true)
  11. .AddEnvironmentVariables();
  12. if (env.IsDevelopment())
  13. {
  14. builder.AddUserSecrets<Startup>();
  15. }
  16. Configuration = builder.Build();
  17. }
  18. public IConfigurationRoot Configuration { get; }
  19. public void ConfigureServices(IServiceCollection services)
  20. {
  21. _moviesApiKey = Configuration["Movies:ServiceApiKey"];
  22. }
  23. public void Configure(IApplicationBuilder app)
  24. {
  25. app.Run(async (context) =>
  26. {
  27. var result = string.IsNullOrEmpty(_moviesApiKey) ? "Null" : "Not Null";
  28. await context.Response.WriteAsync($"Secret is {result}");
  29. });
  30. }
  31. }

映射到 POCO 的机密Map secrets to a POCO

将整个对象文字映射到 POCO (具有属性的简单.NET 类) 可用于聚合相关的属性。

假设应用的机密 json文件包含以下两个机密:

  1. {
  2. "Movies": {
  3. "ServiceApiKey": "12345",
  4. "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  5. }
  6. }

若要将上述机密映射到 POCO,请使用 Configuration API 的对象图形绑定功能。下面的代码绑定到自定义 MovieSettings POCO,并访问 ServiceApiKey 属性值:

  1. var moviesConfig = Configuration.GetSection("Movies")
  2. .Get<MovieSettings>();
  3. _moviesApiKey = moviesConfig.ServiceApiKey;
  1. var moviesConfig = new MovieSettings();
  2. Configuration.GetSection("Movies").Bind(moviesConfig);
  3. _moviesApiKey = moviesConfig.ServiceApiKey;

Movies:ConnectionStringMovies:ServiceApiKey 机密会映射到 MovieSettings中的相应属性:

  1. public class MovieSettings
  2. {
  3. public string ConnectionString { get; set; }
  4. public string ServiceApiKey { get; set; }
  5. }

包含机密的字符串替换String replacement with secrets

以纯文本形式存储密码是不安全。例如,存储在appsettings中的数据库连接字符串可能包含指定用户的密码:

  1. {
  2. "ConnectionStrings": {
  3. "Movies": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;User Id=johndoe;Password=pass123;MultipleActiveResultSets=true"
  4. }
  5. }

更安全的方法是将密码存储为机密。例如:

  1. dotnet user-secrets set "DbPassword" "pass123"

appsettings中的连接字符串中删除 Password 键值对。例如:

  1. {
  2. "ConnectionStrings": {
  3. "Movies": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;User Id=johndoe;MultipleActiveResultSets=true"
  4. }
  5. }

可以对 SqlConnectionStringBuilder 对象的 Password 属性设置机密的值,以完成连接字符串:

  1. public class Startup
  2. {
  3. private string _connection = null;
  4. public Startup(IConfiguration configuration)
  5. {
  6. Configuration = configuration;
  7. }
  8. public IConfiguration Configuration { get; }
  9. public void ConfigureServices(IServiceCollection services)
  10. {
  11. var builder = new SqlConnectionStringBuilder(
  12. Configuration.GetConnectionString("Movies"));
  13. builder.Password = Configuration["DbPassword"];
  14. _connection = builder.ConnectionString;
  15. }
  16. public void Configure(IApplicationBuilder app)
  17. {
  18. app.Run(async (context) =>
  19. {
  20. await context.Response.WriteAsync($"DB Connection: {_connection}");
  21. });
  22. }
  23. }
  1. public class Startup
  2. {
  3. private string _connection = null;
  4. public Startup(IHostingEnvironment env)
  5. {
  6. var builder = new ConfigurationBuilder()
  7. .SetBasePath(env.ContentRootPath)
  8. .AddJsonFile("appsettings.json",
  9. optional: false,
  10. reloadOnChange: true)
  11. .AddEnvironmentVariables();
  12. if (env.IsDevelopment())
  13. {
  14. builder.AddUserSecrets<Startup>();
  15. }
  16. Configuration = builder.Build();
  17. }
  18. public IConfigurationRoot Configuration { get; }
  19. public void ConfigureServices(IServiceCollection services)
  20. {
  21. var builder = new SqlConnectionStringBuilder(
  22. Configuration.GetConnectionString("Movies"));
  23. builder.Password = Configuration["DbPassword"];
  24. _connection = builder.ConnectionString;
  25. }
  26. public void Configure(IApplicationBuilder app)
  27. {
  28. app.Run(async (context) =>
  29. {
  30. await context.Response.WriteAsync($"DB Connection: {_connection}");
  31. });
  32. }
  33. }

列出机密List the secrets

假设应用的机密 json文件包含以下两个机密:

  1. {
  2. "Movies": {
  3. "ServiceApiKey": "12345",
  4. "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  5. }
  6. }

.csproj文件所在的目录运行以下命令:

  1. dotnet user-secrets list

将显示以下输出:

  1. Movies:ConnectionString = Server=(localdb)\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true
  2. Movies:ServiceApiKey = 12345

在前面的示例中,键名称中的冒号表示机密 json中的对象层次结构。

删除单个机密Remove a single secret

假设应用的机密 json文件包含以下两个机密:

  1. {
  2. "Movies": {
  3. "ServiceApiKey": "12345",
  4. "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  5. }
  6. }

.csproj文件所在的目录运行以下命令:

  1. dotnet user-secrets remove "Movies:ConnectionString"

已修改应用的机密 json文件,以删除与 MoviesConnectionString 项关联的键值对:

  1. {
  2. "Movies": {
  3. "ServiceApiKey": "12345"
  4. }
  5. }

运行 dotnet user-secrets list 将显示以下消息:

  1. Movies:ServiceApiKey = 12345

删除所有机密Remove all secrets

假设应用的机密 json文件包含以下两个机密:

  1. {
  2. "Movies": {
  3. "ServiceApiKey": "12345",
  4. "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  5. }
  6. }

.csproj文件所在的目录运行以下命令:

  1. dotnet user-secrets clear

此应用的所有用户密码已从以下文件中删除:

  1. {}

运行 dotnet user-secrets list 将显示以下消息:

  1. No secrets configured for this application.

其他资源Additional resources